#pragma once #include #include "AudioMath.h" #include "IComposite.h" #include "LookupTableFactory.h" #include "MultiLag.h" #include "ObjectCache.h" #include "poly.h" #include "SinOscillator.h" using Osc = SinOscillator; #ifndef _CLAMP #define _CLAMP namespace std { inline float clamp(float v, float lo, float hi) { assert(lo < hi); return std::min(hi, std::max(v, lo)); } } #endif template class CHBDescription : public IComposite { public: Config getParam(int i) override; int getNumParams() override; }; /** * Composite for Chebyshev module. * * Performance measure for 1.0 = 42.44 * reduced polynomial order to what we actually use (10), perf = 39.5 */ template class CHB : public TBase { public: CHB(struct Module * module) : TBase(module) { init(); } CHB() : TBase() { init(); } enum ParamIds { PARAM_TUNE, PARAM_OCTAVE, PARAM_EXTGAIN, PARAM_PITCH_MOD_TRIM, PARAM_LINEAR_FM_TRIM, PARAM_EXTGAIN_TRIM, PARAM_FOLD, PARAM_SLOPE, PARAM_MAG_EVEN, PARAM_MAG_ODD, PARAM_H0, PARAM_H1, PARAM_H2, PARAM_H3, PARAM_H4, PARAM_H5, PARAM_H6, PARAM_H7, PARAM_H8, PARAM_H9, // up to here is Ver 1.0 PARAM_EXPAND, PARAM_RISE, PARAM_FALL, PARAM_EVEN_TRIM, PARAM_ODD_TRIM, PARAM_SLOPE_TRIM, PARAM_SEMIS, NUM_PARAMS }; const int numHarmonics = 1 + PARAM_H9 - PARAM_H0; enum InputIds { CV_INPUT, PITCH_MOD_INPUT, LINEAR_FM_INPUT, ENV_INPUT, GAIN_INPUT, AUDIO_INPUT, SLOPE_INPUT, H0_INPUT, H1_INPUT, H2_INPUT, H3_INPUT, H4_INPUT, H5_INPUT, H6_INPUT, H7_INPUT, H8_INPUT, H9_INPUT, H10_INPUT, // up to here V1.0 RISE_INPUT, FALL_INPUT, EVEN_INPUT, ODD_INPUT, NUM_INPUTS }; enum OutputIds { MIX_OUTPUT, NUM_OUTPUTS }; enum LightIds { GAIN_GREEN_LIGHT, GAIN_RED_LIGHT, NUM_LIGHTS }; /** Implement IComposite */ static std::shared_ptr getDescription() { return std::make_shared>(); } /** * Main processing entry point. Called every sample */ void step() override; void onSampleRateChange() { knobToFilterL = makeLPFDirectFilterLookup(this->engineGetSampleTime()); } float _freq = 0; private: int cycleCount = 1; int clipCount = 0; int signalCount = 0; const int clipDuration = 4000; float finalGain = 0; bool isExternalAudio = false; static const int polyOrder = 10; /** * The waveshaper that is the heart of this module. * Let's use doubles. */ Poly poly; MultiLag<12> lag; /* * maps freq multiple to "octave". * In other words, log base 12. */ float _octave[polyOrder]; float getOctave(int mult) const; void init(); // round up to 12, so multi-lag is happy float _volume[12] = {0}; /** * Internal sine wave oscillator to drive the waveshaper */ SinOscillatorParams sinParams; SinOscillatorState sinState; // just maps 0..1 to 0..1 std::shared_ptr> audioTaper = {ObjectCache::getAudioTaper()}; AudioMath::ScaleFun gainCombiner = AudioMath::makeLinearScaler(0.f, 1.f); std::function expLookup = ObjectCache::getExp2Ex(); std::shared_ptr> db2gain = ObjectCache::getDb2Gain(); std::shared_ptr > knobToFilterL; /** * Audio taper for the slope. */ AudioMath::ScaleFun slopeScale = {AudioMath::makeLinearScaler(-18, 0)}; /** * do one-time calculations when sample rate changes */ void internalUpdate(); /** * Do all the processing to get the input waveform * that will be fed to the polynomials */ float getInput(); void calcVolumes(float *); void checkClipping(float sample); void updateLagTC(); /** * Does audio taper * @param raw = 0..1 * @return 0..1 */ float taper(float raw) { return LookupTable::lookup(*audioTaper, raw, false); } AudioMath::ScaleFun lin = AudioMath::makeLinearScaler(0, 1); }; template inline void CHB::init() { for (int i = 0; i < polyOrder; ++i) { _octave[i] = log2(float(i + 1)); } onSampleRateChange(); lag.setAttack(.1f); lag.setRelease(.0001f); } template inline float CHB::getOctave(int i) const { assert(i >= 0 && i < polyOrder); return _octave[i]; } template inline void CHB::updateLagTC() { const float combinedA = lin( TBase::inputs[RISE_INPUT].value, TBase::params[PARAM_RISE].value, 1); const float combinedR = lin( TBase::inputs[FALL_INPUT].value, TBase::params[PARAM_FALL].value, 1); if (combinedA < .1 && combinedR < .1) { lag.setEnable(false); } else { lag.setEnable(true); const float lA = LookupTable::lookup(*knobToFilterL, combinedA); lag.setAttackL(lA); const float lR = LookupTable::lookup(*knobToFilterL, combinedR); lag.setReleaseL(lR); } } template inline float CHB::getInput() { assert(TBase::engineGetSampleTime() > 0); // Get the frequency from the inputs. float pitch = 1.0f + roundf(TBase::params[PARAM_OCTAVE].value) + TBase::params[PARAM_SEMIS].value / 12.0f + TBase::params[PARAM_TUNE].value / 12.0f; pitch += TBase::inputs[CV_INPUT].value; pitch += .25f * TBase::inputs[PITCH_MOD_INPUT].value * taper(TBase::params[PARAM_PITCH_MOD_TRIM].value); const float q = float(log2(261.626)); // move up to pitch range of EvenVCO pitch += q; _freq = expLookup(pitch); if (_freq < .01f) { _freq = .01f; } // Multiply in the Linear FM contribution _freq *= 1.0f + TBase::inputs[LINEAR_FM_INPUT].value * taper(TBase::params[PARAM_LINEAR_FM_TRIM].value); float time = std::clamp(_freq * TBase::engineGetSampleTime(), -.5f, 0.5f); Osc::setFrequency(sinParams, time); if (cycleCount == 0) { // Get the gain from the envelope generator in // eGain = {0 .. 10.0f } float eGain = TBase::inputs[ENV_INPUT].active ? TBase::inputs[ENV_INPUT].value : 10.f; isExternalAudio = TBase::inputs[AUDIO_INPUT].active; const float gainKnobValue = TBase::params[PARAM_EXTGAIN].value; const float gainCVValue = TBase::inputs[GAIN_INPUT].value; const float gainTrimValue = TBase::params[PARAM_EXTGAIN_TRIM].value; const float combinedGain = gainCombiner(gainCVValue, gainKnobValue, gainTrimValue); // tapered gain {0 .. 0.5} const float taperedGain = .5f * taper(combinedGain); // final gain 0..5 finalGain = taperedGain * eGain; } float input = finalGain * (isExternalAudio ? TBase::inputs[AUDIO_INPUT].value : Osc::run(sinState, sinParams)); checkClipping(input); // Now clip or fold to keep in -1...+1 if (TBase::params[PARAM_FOLD].value > .5) { input = AudioMath::fold(input); } else { input = std::max(input, -1.f); input = std::min(input, 1.f); } return input; } /** * Desired behavior: * If we clip, led goes red and stays red for clipDuration * if not red, sign present goes green * nothing - turn off */ template inline void CHB::checkClipping(float input) { if (input > 1) { // if clipping, go red clipCount = clipDuration; TBase::lights[GAIN_RED_LIGHT].value = 10; TBase::lights[GAIN_GREEN_LIGHT].value = 0; } else if (clipCount) { // If red,run down the clock clipCount--; if (clipCount <= 0) { TBase::lights[GAIN_RED_LIGHT].value = 0; TBase::lights[GAIN_GREEN_LIGHT].value = 0; } } else if (input > .3f) { // if signal present signalCount = clipDuration; TBase::lights[GAIN_GREEN_LIGHT].value = 10; } else if (signalCount) { signalCount--; if (signalCount <= 0) { TBase::lights[GAIN_GREEN_LIGHT].value = 0; } } } template inline void CHB::calcVolumes(float * volumes) { // first get the harmonics knobs, and scale them for (int i = 0; i < numHarmonics; ++i) { float val = taper(TBase::params[i + PARAM_H0].value); // apply taper to the knobs // If input connected, scale and multiply with knob value if (TBase::inputs[i + H0_INPUT].active) { const float inputCV = TBase::inputs[i + H0_INPUT].value * .1f; val *= std::max(inputCV, 0.f); } volumes[i] = val; } // Second: apply the even and odd knobs { const float evenCombined = gainCombiner( TBase::inputs[EVEN_INPUT].value, TBase::params[PARAM_MAG_EVEN].value, TBase::params[PARAM_EVEN_TRIM].value); const float oddCombined = gainCombiner( TBase::inputs[ODD_INPUT].value, TBase::params[PARAM_MAG_ODD].value, TBase::params[PARAM_ODD_TRIM].value); const float even = taper(evenCombined); const float odd = taper(oddCombined); for (int i = 1; i < polyOrder; ++i) { const float mul = (i & 1) ? even : odd; // 0 = fundamental, 1=even, 2=odd.... volumes[i] *= mul; } } // Third: slope { const float slope = slopeScale( TBase::inputs[SLOPE_INPUT].value, TBase::params[PARAM_SLOPE].value, TBase::params[PARAM_SLOPE_TRIM].value); for (int i = 0; i < polyOrder; ++i) { float slopeAttenDb = slope * getOctave(i); float slopeAtten = LookupTable::lookup(*db2gain, slopeAttenDb); volumes[i] *= slopeAtten; } } } template inline void CHB::step() { if (--cycleCount < 0) { cycleCount = 3; } // do all the processing to get the carrier signal // Does the pitch every cycle, vol every 4 const float input = getInput(); if (cycleCount == 0) { updateLagTC(); // TODO: could do at reduced rate calcVolumes(_volume); // now _volume has all 10 harmonic volumes lag.step(_volume); // TODO: we could run lag at full rate. for (int i = 0; i < polyOrder; ++i) { //poly.setGain(i, _volume[i]); poly.setGain(i, lag.get(i)); } } float output = poly.run(input, std::min(finalGain, 1.f)); TBase::outputs[MIX_OUTPUT].value = 5.0f * output; } template int CHBDescription::getNumParams() { return CHB::NUM_PARAMS; } template inline IComposite::Config CHBDescription::getParam(int i) { Config ret(0, 1, 0, ""); const float defaultGainParam = .63108f; switch (i) { case CHB::PARAM_TUNE: ret = {-1.0f, 1.0f, 0, "Fine Tune"}; break; case CHB::PARAM_OCTAVE: ret = {-5.0f, 4.0f, 0.f, "Octave"}; break; case CHB::PARAM_EXTGAIN: ret = {-5.0f, 5.0f, defaultGainParam, "External Gain"}; break; case CHB::PARAM_PITCH_MOD_TRIM: ret = {0, 1.0f, 0.0f, "Pitch mod trim"}; break; case CHB::PARAM_LINEAR_FM_TRIM: ret = {0, 1.0f, 0.0f, "Linear FM trim"}; break; case CHB::PARAM_EXTGAIN_TRIM: ret = {-1, 1, 0, "External gain trim"}; break; case CHB::PARAM_FOLD: ret = {0.0f, 1.0f, 0.0f, "Fold/Clip"}; break; case CHB::PARAM_SLOPE: ret = {-5, 5, 5, "Harmonic slope"}; break; case CHB::PARAM_MAG_EVEN: ret = {-5, 5, 5, "Even harmonic volume"}; break; case CHB::PARAM_MAG_ODD: ret = {-5, 5, 5, "Odd harmonic volume"}; break; case CHB::PARAM_H0: ret = {0.0f, 1.0f, 1, "Fundamental level"}; break; case CHB::PARAM_H1: ret = {0.0f, 1.0f, 0, "Harmonic 1 level"}; break; case CHB::PARAM_H2: ret = {0.0f, 1.0f, 0, "Harmonic 2 level"}; break; case CHB::PARAM_H3: ret = {0.0f, 1.0f, 0, "Harmonic 3 level"}; break; case CHB::PARAM_H4: ret = {0.0f, 1.0f, 0, "Harmonic 4 level"}; break; case CHB::PARAM_H5: ret = {0.0f, 1.0f, 0, "Harmonic 5 level"}; break; case CHB::PARAM_H6: ret = {0.0f, 1.0f, 0, "Harmonic 6 level"}; break; case CHB::PARAM_H7: ret = {0.0f, 1.0f, 0, "Harmonic 7 level"}; break; case CHB::PARAM_H8: ret = {0.0f, 1.0f, 0, "Harmonic 8 level"}; break; case CHB::PARAM_H9: // up to here is Ver 1.0 ret = {0.0f, 1.0f, 0, "Harmonic 9 level"}; break; case CHB::PARAM_EXPAND: ret = {0, 1, 0, "unused"}; break; case CHB::PARAM_RISE: ret = {-5.f, 5.f, 0.f, "Rise time (harmonic volume CV)"}; break; case CHB::PARAM_FALL: ret = {-5.f, 5.f, 0.f, "Fall time (harmonic volume CV)"}; break; case CHB::PARAM_EVEN_TRIM: ret = {-1.0f, 1.0f, 0, "Even Harmonic CV trim"}; break; case CHB::PARAM_ODD_TRIM: ret = {-1.0f, 1.0f, 0, "Odd Harmonic CV trim"}; break; case CHB::PARAM_SLOPE_TRIM: ret = {-1.0f, 1.0f, 0, "Harmonic slope CV trim"}; break; case CHB::PARAM_SEMIS: ret = {-11.0f, 11.0f, 0.f, "Semitone offset"}; break; default: assert(false); } return ret; }