#pragma once #include #include "AudioMath.h" #include "poly.h" #include "ObjectCache.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 /** * 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 CHBg : public TBase { public: CHBg(struct Module * module) : TBase(module) { init(); } CHBg() : 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, 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, NUM_INPUTS }; enum OutputIds { MIX_OUTPUT, NUM_OUTPUTS }; enum LightIds { GAIN_GREEN_LIGHT, GAIN_RED_LIGHT, NUM_LIGHTS }; /** * Main processing entry point. Called every sample */ void step() override; float _freq = 0; private: bool economyMode = true; // let's default to economy mode 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; /* * maps freq multiple to "octave". * In other words, log base 12. */ float _octave[polyOrder]; float getOctave(int mult) const; void init(); float _volume[polyOrder] = {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(); /** * 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); /** * Does audio taper * @param raw = 0..1 * @return 0..1 */ float taper(float raw) { return LookupTable::lookup(*audioTaper, raw, false); } }; template inline void CHBg::init() { for (int i = 0; i < polyOrder; ++i) { _octave[i] = log2(float(i + 1)); } } template inline float CHBg::getOctave(int i) const { assert(i >= 0 && i < polyOrder); return _octave[i]; } template inline float CHBg::getInput() { assert(TBase::engineGetSampleTime() > 0); // Get the frequency from the inputs. float pitch = 1.0f + roundf(TBase::params[PARAM_OCTAVE].value) + 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 CHBg::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 CHBg::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 even = taper(TBase::params[PARAM_MAG_EVEN].value); const float odd = taper(TBase::params[PARAM_MAG_ODD].value); 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::params[PARAM_SLOPE].value, TBase::inputs[SLOPE_INPUT].value, 1); for (int i = 0; i < polyOrder; ++i) { float slopeAttenDb = slope * getOctave(i); float slopeAtten = LookupTable::lookup(*db2gain, slopeAttenDb); volumes[i] *= slopeAtten; } } } template inline void CHBg::step() { if (economyMode) { if (--cycleCount < 0) { cycleCount = 3; } } else { cycleCount = 0; } // do all the processing to get the carrier signal const float input = getInput(); if (cycleCount == 0) { calcVolumes(_volume); for (int i = 0; i < polyOrder; ++i) { poly.setGain(i, _volume[i]); } } float output = poly.run(input, std::min(finalGain, 1.f)); TBase::outputs[MIX_OUTPUT].value = 5.0f * output; }