#pragma once #include "GateTrigger.h" #include "IIRDecimator.h" #include "NonUniformLookupTable.h" #include "ObjectCache.h" #include "StateVariable4PHP.h" class SawtoothDetuneCurve { public: /** * @param depth is "detune" knob. 0..1 * returns a number such that freq = detuneFactor * initialFreq */ float getDetuneFactor(float depth) { return NonUniformLookupTable::lookup(table, depth); } SawtoothDetuneCurve() { // this data is pretty regular - could use uniform table using T = NonUniformLookupTable; T::addPoint(table, 0, 0); T::addPoint(table, .0551f, .00967f); T::addPoint(table, .118f, .022f); T::addPoint(table, .181f, .04f); T::addPoint(table, .244f, .0467f); T::addPoint(table, .307f, .059f); T::addPoint(table, .37f, .0714f); T::addPoint(table, .433f, .0838f); T::addPoint(table, .496f, .0967f); T::addPoint(table, .559f, .121f); T::addPoint(table, .622f, .147f); T::addPoint(table, .748f, .243f); T::addPoint(table, .811f, .293f); T::addPoint(table, .874f, .343f); T::addPoint(table, .937f, .392f); T::addPoint(table, 1, 1); NonUniformLookupTable::finalize(table); } private: NonUniformLookupTableParams table; }; /** * orig CPU = 39 * sub sample => 16 * beta1 => 16.1 17.7 if change pitch every 16 samples. * beta 3: fix instability * made semitone and fine ranges sane. */ template class Super : public TBase { public: Super(struct Module * module) : TBase(module), gateTrigger(true) { init(); } Super() : TBase(), gateTrigger(true) { init(); } /** * re-calculate everything that changes with sample * rate. Also everything that depends on baseFrequency. * * Only needs to be called once. */ void init(); enum ParamIds { OCTAVE_PARAM, SEMI_PARAM, FINE_PARAM, DETUNE_PARAM, DETUNE_TRIM_PARAM, MIX_PARAM, MIX_TRIM_PARAM, FM_PARAM, CLEAN_PARAM, NUM_PARAMS }; enum InputIds { CV_INPUT, TRIGGER_INPUT, DETUNE_INPUT, MIX_INPUT, FM_INPUT, NUM_INPUTS }; enum OutputIds { MAIN_OUTPUT, NUM_OUTPUTS }; enum LightIds { NUM_LIGHTS }; /** * Main processing entry point. Called every sample */ void step() override; private: static const unsigned int MAX_OVERSAMPLE = 16; static const int numSaws = 7; float phase[numSaws] = {0}; float phaseInc[numSaws] = {0}; float globalPhaseInc = 0; std::function expLookup = ObjectCache::getExp2Ex(); std::shared_ptr> audioTaper = ObjectCache::getAudioTaper(); // knob, cv, trim -> 0..1 AudioMath::ScaleFun scaleDetune; float runSaws(); void updatePhaseInc(); void updateAudioClassic(); void updateAudioClean(); void updateTrigger(); void updateMix(); int getOversampleRate(); AudioMath::RandomUniformFunc random = AudioMath::random(); int inputSubSampleCounter = 1; const static int inputSubSample = 4; // only look at knob/cv every 4 // TODO: make static float const detuneFactors[numSaws] = { .89f, .94f, .98f, 1.f, 1.02f, 1.06f, 1.107f }; void updateHPFilters(); SawtoothDetuneCurve detuneCurve; GateTrigger gateTrigger; float gainCenter = 0; float gainSides = 0; StateVariable4PHP hpf; float buffer[MAX_OVERSAMPLE] = {}; IIRDecimator decimator; }; template inline void Super::init() { scaleDetune = AudioMath::makeLinearScaler(0, 1); const int rate = getOversampleRate(); const int decimateDiv = std::max(rate, (int) MAX_OVERSAMPLE); decimator.setup(decimateDiv); } template inline int Super::getOversampleRate() { int rate = 1; const int setting = (int) std::round(TBase::params[CLEAN_PARAM].value); switch (setting) { case 0: rate = 1; break; case 1: rate = 4; break; case 2: rate = 16; break; default: assert(false); } assert(rate <= (int) MAX_OVERSAMPLE); return rate; } template inline void Super::updatePhaseInc() { const float cv = TBase::inputs[CV_INPUT].value; const float finePitch = TBase::params[FINE_PARAM].value / 12.0f; const float semiPitch = TBase::params[SEMI_PARAM].value / 12.0f; float pitch = 1.0f + roundf(TBase::params[OCTAVE_PARAM].value) + semiPitch + finePitch; pitch += cv; const float fm = TBase::inputs[FM_INPUT].value; const float fmDepth = AudioMath::quadraticBipolar(TBase::params[FM_PARAM].value); pitch += (fmDepth * fm); const float q = float(log2(261.626)); // move up to pitch range of EvenVCO pitch += q; const float freq = expLookup(pitch); globalPhaseInc = TBase::engineGetSampleTime() * freq; const float rawDetuneValue = scaleDetune( TBase::inputs[DETUNE_INPUT].value, TBase::params[DETUNE_PARAM].value, TBase::params[DETUNE_TRIM_PARAM].value); const float detuneInput = detuneCurve.getDetuneFactor(rawDetuneValue); // const bool classic = TBase::params[CLEAN_PARAM].value < .5f; const int oversampleRate = getOversampleRate(); for (int i = 0; i < numSaws; ++i) { float detune = (detuneFactors[i] - 1) * detuneInput; detune += 1; float phaseIncI = globalPhaseInc * detune; phaseIncI = std::min(phaseIncI, .4f); // limit so saws don't go crazy if (oversampleRate > 1) { phaseIncI /= oversampleRate; } phaseInc[i] = phaseIncI; } } template inline float Super::runSaws() { float mix = 0; for (int i = 0; i < numSaws; ++i) { phase[i] += phaseInc[i]; if (phase[i] > 1) { phase[i] -= 1; } assert(phase[i] <= 1); assert(phase[i] >= 0); const float gain = (i == numSaws / 2) ? gainCenter : gainSides; // mix += phase[i] * gain; mix += (phase[i] - .5f) * gain; // experiment to get rid of DC } mix *= 4.5; // too low 2 too high 10 return mix; } template inline void Super::updateAudioClassic() { const float mix = runSaws(); const float output = hpf.run(mix); TBase::outputs[MAIN_OUTPUT].value = output; } template inline void Super::updateAudioClean() { const int bufferSize = getOversampleRate(); decimator.setup(bufferSize); for (int i = 0; i < bufferSize; ++i) { const float mix = runSaws(); buffer[i] = mix; } //const float output = hpf.run(mix); const float output = decimator.process(buffer); TBase::outputs[MAIN_OUTPUT].value = output; } template inline void Super::updateHPFilters() { const float filterCutoff = std::min(globalPhaseInc, .1f); hpf.setCutoff(filterCutoff); } template inline void Super::step() { updateTrigger(); if (--inputSubSampleCounter <= 0) { inputSubSampleCounter = inputSubSample; updatePhaseInc(); updateHPFilters(); updateMix(); } // const bool classic = TBase::params[CLEAN_PARAM].value < .5f; int rate = getOversampleRate(); if (rate == 1) { updateAudioClassic(); } else { updateAudioClean(); } } template inline void Super::updateTrigger() { gateTrigger.go(TBase::inputs[TRIGGER_INPUT].value); if (gateTrigger.trigger()) { for (int i = 0; i < numSaws; ++i) { phase[i] = this->random(); } } } template inline void Super::updateMix() { const float rawMixValue = scaleDetune( TBase::inputs[MIX_INPUT].value, TBase::params[MIX_PARAM].value, TBase::params[MIX_TRIM_PARAM].value); gainCenter = -0.55366f * rawMixValue + 0.99785f; gainSides = -0.73764f * rawMixValue * rawMixValue + 1.2841f * rawMixValue + 0.044372f; }