#pragma once #include "MinBLEPVCO.h" #include "ObjectCache.h" #include "SqMath.h" /** * perf test 1.0 44.5 * 44.7 with normalization */ template class EV3 : public TBase { public: friend class TestMB; EV3(struct Module * module) : TBase(module) { init(); } EV3() : TBase() { init(); } enum class Waves { SIN, TRI, SAW, SQUARE, EVEN, NONE, END // just a marker }; enum ParamIds { MIX1_PARAM, MIX2_PARAM, MIX3_PARAM, OCTAVE1_PARAM, SEMI1_PARAM, FINE1_PARAM, FM1_PARAM, SYNC1_PARAM, WAVE1_PARAM, PW1_PARAM, PWM1_PARAM, OCTAVE2_PARAM, SEMI2_PARAM, FINE2_PARAM, FM2_PARAM, SYNC2_PARAM, WAVE2_PARAM, PW2_PARAM, PWM2_PARAM, OCTAVE3_PARAM, SEMI3_PARAM, FINE3_PARAM, FM3_PARAM, SYNC3_PARAM, WAVE3_PARAM, PW3_PARAM, PWM3_PARAM, NUM_PARAMS }; enum InputIds { CV1_INPUT, CV2_INPUT, CV3_INPUT, FM1_INPUT, FM2_INPUT, FM3_INPUT, PWM1_INPUT, PWM2_INPUT, PWM3_INPUT, NUM_INPUTS }; enum OutputIds { MIX_OUTPUT, VCO1_OUTPUT, VCO2_OUTPUT, VCO3_OUTPUT, NUM_OUTPUTS }; enum LightIds { NUM_LIGHTS }; void step() override; bool isLoweringVolume() const { return volumeScale < 1; } private: void setSync(); void processPitchInputs(); void processPitchInputs(int osc); void processWaveforms(); void stepVCOs(); void init(); void processPWInputs(); void processPWInput(int osc); float getInput(int osc, InputIds in0, InputIds in1, InputIds in2); MinBLEPVCO vcos[3]; float _freq[3]; float _out[3]; float volumeScale = 1; std::function expLookup = ObjectCache::getExp2Ex(); std::shared_ptr> audioTaper = ObjectCache::getAudioTaper(); }; template inline void EV3::init() { for (int i = 0; i < 3; ++i) { vcos[i].setWaveform(MinBLEPVCO::Waveform::Saw); } vcos[0].setSyncCallback([this](float f) { if (TBase::params[SYNC2_PARAM].value > .5) { vcos[1].onMasterSync(f); } if (TBase::params[SYNC3_PARAM].value > .5) { vcos[2].onMasterSync(f); } }); } template inline void EV3::setSync() { vcos[0].setSyncEnabled(false); vcos[1].setSyncEnabled(TBase::params[SYNC2_PARAM].value > .5); vcos[2].setSyncEnabled(TBase::params[SYNC3_PARAM].value > .5); } template inline void EV3::processWaveforms() { vcos[0].setWaveform((MinBLEPVCO::Waveform)(int)TBase::params[WAVE1_PARAM].value); vcos[1].setWaveform((MinBLEPVCO::Waveform)(int)TBase::params[WAVE2_PARAM].value); vcos[2].setWaveform((MinBLEPVCO::Waveform)(int)TBase::params[WAVE3_PARAM].value); } template float EV3::getInput(int osc, InputIds in1, InputIds in2, InputIds in3) { const bool in2Connected = TBase::inputs[in2].active; const bool in3Connected = TBase::inputs[in3].active; InputIds id = in1; if ((osc == 1) && in2Connected) { id = in2; } if (osc == 2) { if (in3Connected) id = in3; else if (in2Connected) id = in2; } return TBase::inputs[id].value; } template void EV3::processPWInput(int osc) { const float pwmInput = getInput(osc, PWM1_INPUT, PWM2_INPUT, PWM3_INPUT) / 5.f; const int delta = osc * (OCTAVE2_PARAM - OCTAVE1_PARAM); const float pwmTrim = TBase::params[PWM1_PARAM + delta].value; const float pwInit = TBase::params[PW1_PARAM + delta].value; float pw = pwInit + pwmInput * pwmTrim; const float minPw = 0.05f; pw = sq::rescale(std::clamp(pw, -1.0f, 1.0f), -1.0f, 1.0f, minPw, 1.0f - minPw); vcos[osc].setPulseWidth(pw); } template inline void EV3::processPWInputs() { processPWInput(0); processPWInput(1); processPWInput(2); } template inline void EV3::step() { setSync(); processPitchInputs(); processWaveforms(); processPWInputs(); stepVCOs(); float mix = 0; float totalGain = 0; for (int i = 0; i < 3; ++i) { const float knob = TBase::params[MIX1_PARAM + i].value; const float gain = LookupTable::lookup(*audioTaper, knob, false); const float rawWaveform = vcos[i].getOutput(); const float scaledWaveform = rawWaveform * gain; totalGain += gain; mix += scaledWaveform; _out[i] = scaledWaveform; TBase::outputs[VCO1_OUTPUT + i].value = rawWaveform; } if (totalGain <= 1) { volumeScale = 1; } else { volumeScale = 1.0f / totalGain; } mix *= volumeScale; TBase::outputs[MIX_OUTPUT].value = mix; } template inline void EV3::stepVCOs() { for (int i = 0; i < 3; ++i) { vcos[i].step(); } } template inline void EV3::processPitchInputs() { float lastFM = 0; for (int osc = 0; osc < 3; ++osc) { assert(osc >= 0 && osc <= 2); const int delta = osc * (OCTAVE2_PARAM - OCTAVE1_PARAM); const float cv = getInput(osc, CV1_INPUT, CV2_INPUT, CV3_INPUT); const float finePitch = TBase::params[FINE1_PARAM + delta].value / 12.0f; const float semiPitch = TBase::params[SEMI1_PARAM + delta].value / 12.0f; float pitch = 1.0f + roundf(TBase::params[OCTAVE1_PARAM + delta].value) + semiPitch + finePitch; pitch += cv; float fmCombined = 0; // The final, scaled, value (post knob if (TBase::inputs[FM1_INPUT + osc].active) { const float fm = TBase::inputs[FM1_INPUT + osc].value; const float fmDepth = AudioMath::quadraticBipolar(TBase::params[FM1_PARAM + delta].value); fmCombined = (fmDepth * fm); } else { fmCombined = lastFM; } pitch += fmCombined; lastFM = fmCombined; const float q = float(log2(261.626)); // move up to pitch range of EvenVCO pitch += q; const float freq = expLookup(pitch); _freq[osc] = freq; vcos[osc].setNormalizedFreq(TBase::engineGetSampleTime() * freq, TBase::engineGetSampleTime()); } }