From c199a9b42f367ede16d94148569c638d0abd82fa Mon Sep 17 00:00:00 2001 From: Blitblank Date: Sat, 17 Jan 2026 21:41:37 -0600 Subject: [PATCH] cleanup oscillators --- src/ParameterStore.h | 54 ++++++++++++++++-------- src/synth/Oscillator.cpp | 9 ++-- src/synth/Oscillator.h | 6 ++- src/synth/Voice.cpp | 13 ++++-- src/synth/WavetableController.cpp | 70 +++++++++++++++++++++++++++++++ src/synth/WavetableController.h | 34 +++++++++++++++ 6 files changed, 157 insertions(+), 29 deletions(-) create mode 100644 src/synth/WavetableController.cpp create mode 100644 src/synth/WavetableController.h diff --git a/src/ParameterStore.h b/src/ParameterStore.h index bef7002..70fdef1 100644 --- a/src/ParameterStore.h +++ b/src/ParameterStore.h @@ -9,6 +9,15 @@ enum class ParamId : uint16_t { Osc1Frequency, Osc1WaveSelector1, Osc1WaveSelector2, + Osc1OctaveOffset, + Osc1SemitoneOffset, + Osc1PitchOffset, + Osc2OctaveOffset, + Osc2SemitoneOffset, + Osc2PitchOffset, + Osc3OctaveOffset, + Osc3SemitoneOffset, + Osc3PitchOffset, Osc1VolumeDepth, Osc1VolumeEnvA, Osc1VolumeEnvD, @@ -63,24 +72,33 @@ struct ParamDefault { // TODO: make these configurable via yml file too // later TODO: and then when I have full on profile saving there will be a default profile to load from constexpr std::array(ParamId::Count)> PARAM_DEFS {{ - { 100.0f, 20.0f, 600.0f}, // Osc1Freq - { 2.0f, 0.0f, 0.0f}, // Osc1WaveSelector1 - { 1.0f, 0.0f, 0.0f}, // Osc1WaveSelector2 - { 1.0f, 0.0f, 2.0f}, // Osc1VolumeDepth - { 0.05f, 0.0f, 2.0f}, // Osc1VolumeEnvA - { 0.2f, 0.0f, 2.0f}, // Osc1VolumeEnvD - { 0.7f, 0.0f, 1.0f}, // Osc1VolumeEnvS - { 0.2f, 0.0f, 2.0f}, // Osc1VolumeEnvR - { 4.0f, 0.0f, 8.0f}, // FilterCutoffDepth - { 0.05f, 0.0f, 2.0f}, // FilterCutoffEnvA - { 0.20f, 0.0f, 2.0f}, // FilterCutoffEnvD - { 0.2f, 0.0f, 1.0f}, // FilterCutoffEnvS - { 0.25f, 0.0f, 2.0f}, // FilterCutoffEnvR - { 3.0f, 0.0f, 8.0f}, // FilterResonanceDepth - { 0.05f, 0.0f, 2.0f}, // FilterResonanceEnvA - { 0.20f, 0.0f, 2.0f}, // FilterResonanceEnvD - { 0.5f, 0.0f, 1.0f}, // FilterResonanceEnvS - { 0.30f, 0.0f, 2.0f}, // FilterResonanceEnvR + { 100.0f, 20.0f, 600.0f}, // Osc1Freq + { 2.0f, 0.0f, 0.0f}, // OscWaveSelector1 + { 1.0f, 0.0f, 0.0f}, // OscWaveSelector2 + { 0.0f, -5.0f, 5.0f}, // Osc1OctaveOffset + { 0.0f, -12.0f, 12.0f}, // Osc1SemitoneOffset + { 0.0f, -100.0f, 100.0f}, // Osc1PitchOffset + { 1.0f, -5.0f, 5.0f}, // Osc2OctaveOffset + { 0.0f, -12.0f, 12.0f}, // Osc2SemitoneOffset + { 0.0f, -100.0f, 100.0f}, // Osc2PitchOffset + { 1.0f, -5.0f, 5.0f}, // Osc3OctaveOffset + { 7.0f, -12.0f, 12.0f}, // Osc3SemitoneOffset + { 1.96f, -100.0f, 100.0f}, // Osc3PitchOffset + { 1.0f, 0.0f, 2.0f}, // Osc1VolumeDepth + { 0.05f, 0.0f, 2.0f}, // Osc1VolumeEnvA + { 0.2f, 0.0f, 2.0f}, // Osc1VolumeEnvD + { 0.7f, 0.0f, 1.0f}, // Osc1VolumeEnvS + { 0.2f, 0.0f, 2.0f}, // Osc1VolumeEnvR + { 4.0f, 0.0f, 8.0f}, // FilterCutoffDepth + { 0.05f, 0.0f, 2.0f}, // FilterCutoffEnvA + { 0.20f, 0.0f, 2.0f}, // FilterCutoffEnvD + { 0.2f, 0.0f, 1.0f}, // FilterCutoffEnvS + { 0.25f, 0.0f, 2.0f}, // FilterCutoffEnvR + { 3.0f, 0.0f, 8.0f}, // FilterResonanceDepth + { 0.05f, 0.0f, 2.0f}, // FilterResonanceEnvA + { 0.20f, 0.0f, 2.0f}, // FilterResonanceEnvD + { 0.5f, 0.0f, 1.0f}, // FilterResonanceEnvS + { 0.30f, 0.0f, 2.0f}, // FilterResonanceEnvR }}; constexpr size_t PARAM_COUNT = static_cast(ParamId::Count); diff --git a/src/synth/Oscillator.cpp b/src/synth/Oscillator.cpp index 2a746de..e1e5f80 100644 --- a/src/synth/Oscillator.cpp +++ b/src/synth/Oscillator.cpp @@ -17,8 +17,8 @@ float Oscillator::frequency() { return frequency_; } -float Oscillator::process(uint8_t note, bool& scopeTrigger) { - frequency_ = noteToFrequency(note); +float Oscillator::process(uint8_t note, float detune, bool& scopeTrigger) { + frequency_ = noteToFrequency(note, detune); return process(frequency_, scopeTrigger); } @@ -38,7 +38,6 @@ float Oscillator::process(float frequency, bool& scopeTrigger) { 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)); +inline float Oscillator::noteToFrequency(uint8_t note, float detune) { + return SYNTH_PITCH_STANDARD * pow(2.0f, (static_cast(note - SYNTH_MIDI_HOME) + detune) / static_cast(SYNTH_NOTES_PER_OCTAVE)); } diff --git a/src/synth/Oscillator.h b/src/synth/Oscillator.h index 0fbdc71..ba78c30 100644 --- a/src/synth/Oscillator.h +++ b/src/synth/Oscillator.h @@ -11,6 +11,8 @@ #define M_PI 3.14159265358979323846 #endif +#define SYNTH_TWELFTH_ROOT_TWO 1.05946309435929526463 + // 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 @@ -27,14 +29,14 @@ public: void setSampleRate(float sampleRate); float frequency(); - float process(uint8_t note, bool& scopeTrigger); + float process(uint8_t note, float detune, bool& scopeTrigger); // detune (-1, 1) float process(float frequency, bool& scopeTrigger); private: float sampleRate_ = 44100.0f; - inline float noteToFrequency(uint8_t note); + inline float noteToFrequency(uint8_t note, float detune); // internal state tracking float phase_ = 0.0f; diff --git a/src/synth/Voice.cpp b/src/synth/Voice.cpp index 04f6342..11b9ba2 100644 --- a/src/synth/Voice.cpp +++ b/src/synth/Voice.cpp @@ -87,12 +87,17 @@ float Voice::process(float* params, bool& scopeTrigger) { o.setWavetable(osc1Wave); } - // TODO: oscillator pitch offset + // calculate the note/pitch of the oscillators bool temp = false; - float osc1 = oscillators_[0].process(note_, scopeTrigger); - float osc2 = oscillators_[1].process(static_cast(note_+12), temp); - float osc3 = oscillators_[2].process(static_cast(note_+19), temp); + uint8_t osc1NoteOffset = static_cast((SYNTH_NOTES_PER_OCTAVE+1) * getParam(ParamId::Osc1OctaveOffset) + getParam(ParamId::Osc1SemitoneOffset)); + uint8_t osc2NoteOffset = static_cast((SYNTH_NOTES_PER_OCTAVE+1) * getParam(ParamId::Osc2OctaveOffset) + getParam(ParamId::Osc2SemitoneOffset)); + uint8_t osc3NoteOffset = static_cast((SYNTH_NOTES_PER_OCTAVE+1) * getParam(ParamId::Osc3OctaveOffset) + getParam(ParamId::Osc3SemitoneOffset)); + // sample oscillators + float osc1 = oscillators_[0].process(osc1NoteOffset + note_, getParam(ParamId::Osc1PitchOffset)/100.0f, scopeTrigger); + float osc2 = oscillators_[1].process(osc2NoteOffset + note_, getParam(ParamId::Osc2PitchOffset)/100.0f, temp); + float osc3 = oscillators_[2].process(osc3NoteOffset + note_, getParam(ParamId::Osc3PitchOffset)/100.0f, temp); + // mix oscillators float sampleOut = (osc1 + osc2*0.5f + osc3*0.25f) * gain; // filter sample diff --git a/src/synth/WavetableController.cpp b/src/synth/WavetableController.cpp new file mode 100644 index 0000000..791d81d --- /dev/null +++ b/src/synth/WavetableController.cpp @@ -0,0 +1,70 @@ + +#include "WavetableController.h" + +#include +#include + +WavetableController::WavetableController() { + // load from files + + init(); + + std::cout << "wavetable init" << std::endl; + +} + +void WavetableController::init() { + + wavetables_.resize(4); // resize for however many files we find + + float phase = 0.0f; + float phaseInc = 2.0f * M_PI / static_cast(SYNTH_WAVETABLE_SIZE); + + for(int i = 0; i < SYNTH_WAVETABLE_SIZE; i++) { + + wavetables_[0][i] = std::sin(phase) / 0.707f; // sine + wavetables_[1][i] = (phase >= M_PI) ? 1.0f : -1.0f; // square + wavetables_[2][i] = ((phase / M_PI) - 1.0f) / 0.577f; // saw + + // triangle + float tri = 0.0f; + if(phase <= M_PI/2.0f) { + tri = phase * 2.0f/M_PI; + } else if(phase <= 3.0f*M_PI/2.0f) { + tri = phase * -2.0f/M_PI + 2.0f; + } else { + tri = phase * 2.0f/M_PI - 4.0f; + } + wavetables_[3][i] = tri / 0.577f; + + phase += phaseInc; + } + +} + +float WavetableController::sample(uint8_t wavetableIndex, float phase) { + + float sampleValue = 0.0f; + + if(phase >= 0.0f) { + while(phase >= 2.0f*M_PI) { + phase -= 2.0f*M_PI; + } + } else { + while(phase <= 0.0f*M_PI) { + phase += 2.0f*M_PI; + } + } + + float scaledPhase = phase * static_cast(SYNTH_WAVETABLE_SIZE) / (2.0f*M_PI); + uint32_t index = static_cast(round(scaledPhase)); + if(index >= SYNTH_WAVETABLE_SIZE) index = SYNTH_WAVETABLE_SIZE - 1; + + if(wavetableIndex >= 4) { + wavetableIndex = 3; + } else if(wavetableIndex < 0) { + wavetableIndex = 0; + } + + return wavetables_[wavetableIndex][index]; +} \ No newline at end of file diff --git a/src/synth/WavetableController.h b/src/synth/WavetableController.h new file mode 100644 index 0000000..f03d835 --- /dev/null +++ b/src/synth/WavetableController.h @@ -0,0 +1,34 @@ + +#pragma once + +#include +#include + +#define SYNTH_WAVETABLE_SIZE 2048 +#ifndef M_PI // I hate my stupid chungus life + #define M_PI 3.14159265358979323846 +#endif + +typedef std::array Wavetable; + +class WavetableController { + +private: + /* data */ +public: + + WavetableController(); + ~WavetableController() = default; + + void init(); + + // phase = [0, 2pi) + float sample(uint8_t wavetableIndex, float phase); + +private: + + std::vector wavetables_; + +}; + +