| @@ -4,6 +4,9 @@ | |||
| ## v2.6.0 (in progress) | |||
| * Midi Thing 2 | |||
| * Initial release | |||
| * Octaves | |||
| * Initial release | |||
| ## v2.5.0 | |||
| * Burst | |||
| @@ -23,18 +23,6 @@ | |||
| "Polyphonic" | |||
| ] | |||
| }, | |||
| { | |||
| "slug": "EvenVCO2", | |||
| "name": "Even VCO (beta)", | |||
| "description": "Oscillator including even-harmonic waveform", | |||
| "manualUrl": "https://www.befaco.org/even-vco/", | |||
| "modularGridUrl": "https://www.modulargrid.net/e/befaco-even-vco-", | |||
| "tags": [ | |||
| "VCO", | |||
| "Hardware clone", | |||
| "Polyphonic" | |||
| ] | |||
| }, | |||
| { | |||
| "slug": "Rampage", | |||
| "name": "Rampage", | |||
| @@ -331,6 +319,17 @@ | |||
| "Polyphonic", | |||
| "Utility" | |||
| ] | |||
| }, | |||
| { | |||
| "slug": "Octaves", | |||
| "name": "Octaves", | |||
| "description": "A harsh and funky take of an additive Oscillator.", | |||
| "manualUrl": "https://www.befaco.org/octaves-vco/", | |||
| "modularGridUrl": "https://www.modulargrid.net/e/befaco-octaves-vco", | |||
| "tags": [ | |||
| "Hardware clone", | |||
| "VCO" | |||
| ] | |||
| } | |||
| ] | |||
| } | |||
| @@ -1,331 +0,0 @@ | |||
| #include "plugin.hpp" | |||
| #include "ChowDSP.hpp" | |||
| using simd::float_4; | |||
| struct EvenVCO2 : Module { | |||
| enum ParamIds { | |||
| OCTAVE_PARAM, | |||
| TUNE_PARAM, | |||
| PWM_PARAM, | |||
| NUM_PARAMS | |||
| }; | |||
| enum InputIds { | |||
| PITCH1_INPUT, | |||
| PITCH2_INPUT, | |||
| FM_INPUT, | |||
| SYNC_INPUT, | |||
| PWM_INPUT, | |||
| NUM_INPUTS | |||
| }; | |||
| enum OutputIds { | |||
| TRI_OUTPUT, | |||
| SINE_OUTPUT, | |||
| EVEN_OUTPUT, | |||
| SAW_OUTPUT, | |||
| SQUARE_OUTPUT, | |||
| NUM_OUTPUTS | |||
| }; | |||
| float_4 phase[4] = {}; | |||
| dsp::TSchmittTrigger<float_4> syncTrigger[4]; | |||
| bool removePulseDC = true; | |||
| bool limitPW = true; | |||
| EvenVCO2() { | |||
| config(NUM_PARAMS, NUM_INPUTS, NUM_OUTPUTS); | |||
| configParam(OCTAVE_PARAM, -5.0, 4.0, 0.0, "Octave", "'", 0.5); | |||
| getParamQuantity(OCTAVE_PARAM)->snapEnabled = true; | |||
| configParam(TUNE_PARAM, -7.0, 7.0, 0.0, "Tune", " semitones"); | |||
| configParam(PWM_PARAM, -1.0, 1.0, 0.0, "Pulse width"); | |||
| configInput(PITCH1_INPUT, "Pitch 1"); | |||
| configInput(PITCH2_INPUT, "Pitch 2"); | |||
| configInput(FM_INPUT, "FM"); | |||
| configInput(SYNC_INPUT, "Sync"); | |||
| configInput(PWM_INPUT, "Pulse Width Modulation"); | |||
| configOutput(TRI_OUTPUT, "Triangle"); | |||
| configOutput(SINE_OUTPUT, "Sine"); | |||
| configOutput(EVEN_OUTPUT, "Even"); | |||
| configOutput(SAW_OUTPUT, "Sawtooth"); | |||
| configOutput(SQUARE_OUTPUT, "Square"); | |||
| // calculate up/downsampling rates | |||
| onSampleRateChange(); | |||
| } | |||
| void onSampleRateChange() override { | |||
| float sampleRate = APP->engine->getSampleRate(); | |||
| for (int i = 0; i < NUM_OUTPUTS; ++i) { | |||
| for (int c = 0; c < 4; c++) { | |||
| oversampler[i][c].setOversamplingIndex(oversamplingIndex); | |||
| oversampler[i][c].reset(sampleRate); | |||
| } | |||
| } | |||
| const float lowFreqRegime = oversampler[0][0].getOversamplingRatio() * 1e-3 * sampleRate; | |||
| DEBUG("Low freq regime: %g", lowFreqRegime); | |||
| } | |||
| float_4 aliasSuppressedTri(float_4* phases) { | |||
| float_4 triBuffer[3]; | |||
| for (int i = 0; i < 3; ++i) { | |||
| float_4 p = 2 * phases[i] - 1.0; // range -1.0 to +1.0 | |||
| float_4 s = 0.5 - simd::abs(p); // eq 30 | |||
| triBuffer[i] = (s * s * s - 0.75 * s) / 3.0; // eq 29 | |||
| } | |||
| return (triBuffer[0] - 2.0 * triBuffer[1] + triBuffer[2]); | |||
| } | |||
| float_4 aliasSuppressedSaw(float_4* phases) { | |||
| float_4 sawBuffer[3]; | |||
| for (int i = 0; i < 3; ++i) { | |||
| float_4 p = 2 * phases[i] - 1.0; // range -1 to +1 | |||
| sawBuffer[i] = (p * p * p - p) / 6.0; // eq 11 | |||
| } | |||
| return (sawBuffer[0] - 2.0 * sawBuffer[1] + sawBuffer[2]); | |||
| } | |||
| float_4 aliasSuppressedDoubleSaw(float_4* phases) { | |||
| float_4 sawBuffer[3]; | |||
| for (int i = 0; i < 3; ++i) { | |||
| float_4 p = 4.0 * simd::ifelse(phases[i] < 0.5, phases[i], phases[i] - 0.5) - 1.0; | |||
| sawBuffer[i] = (p * p * p - p) / 24.0; // eq 11 (modified for doubled freq) | |||
| } | |||
| return (sawBuffer[0] - 2.0 * sawBuffer[1] + sawBuffer[2]); | |||
| } | |||
| float_4 aliasSuppressedOffsetSaw(float_4* phases, float_4 pw) { | |||
| float_4 sawOffsetBuff[3]; | |||
| for (int i = 0; i < 3; ++i) { | |||
| float_4 p = 2 * phases[i] - 1.0; // range -1 to +1 | |||
| float_4 pwp = p + 2 * pw; // phase after pw (pw in [0, 1]) | |||
| pwp += simd::ifelse(pwp > 1, -2, 0); // modulo on [-1, +1] | |||
| sawOffsetBuff[i] = (pwp * pwp * pwp - pwp) / 6.0; // eq 11 | |||
| } | |||
| return (sawOffsetBuff[0] - 2.0 * sawOffsetBuff[1] + sawOffsetBuff[2]); | |||
| } | |||
| chowdsp::VariableOversampling<6, float_4> oversampler[NUM_OUTPUTS][4]; // uses a 2*6=12th order Butterworth filter | |||
| int oversamplingIndex = 1; // default is 2^oversamplingIndex == x2 oversampling | |||
| void process(const ProcessArgs& args) override { | |||
| // pitch inputs determine number of polyphony engines | |||
| const int channels = std::max({1, inputs[PITCH1_INPUT].getChannels(), inputs[PITCH2_INPUT].getChannels()}); | |||
| const float pitchKnobs = 1.f + std::round(params[OCTAVE_PARAM].getValue()) + params[TUNE_PARAM].getValue() / 12.f; | |||
| const int oversamplingRatio = oversampler[0][0].getOversamplingRatio(); | |||
| for (int c = 0; c < channels; c += 4) { | |||
| float_4 pw = simd::clamp(params[PWM_PARAM].getValue() + inputs[PWM_INPUT].getPolyVoltageSimd<float_4>(c) / 5.f, -1.f, 1.f); | |||
| if (limitPW) { | |||
| pw = simd::rescale(pw, -1, +1, 0.05f, 0.95f); | |||
| } | |||
| else { | |||
| pw = simd::rescale(pw, -1.f, +1.f, 0.f, 1.f); | |||
| } | |||
| const float_4 fmVoltage = inputs[FM_INPUT].getPolyVoltageSimd<float_4>(c) * 0.25f; | |||
| const float_4 pitch = inputs[PITCH1_INPUT].getPolyVoltageSimd<float_4>(c) + inputs[PITCH2_INPUT].getPolyVoltageSimd<float_4>(c); | |||
| const float_4 freq = dsp::FREQ_C4 * simd::pow(2.f, pitchKnobs + pitch + fmVoltage); | |||
| const float_4 deltaBasePhase = simd::clamp(freq * args.sampleTime / oversamplingRatio, 1e-6, 0.5f); | |||
| // floating point arithmetic doesn't work well at low frequencies, specifically because the finite difference denominator | |||
| // becomes tiny - we check for that scenario and use naive / 1st order waveforms in that frequency regime (as aliasing isn't | |||
| // a problem there). With no oversampling, at 44100Hz, the threshold frequency is 44.1Hz. | |||
| const float_4 lowFreqRegime = simd::abs(deltaBasePhase) < 1e-3; | |||
| // 1 / denominator for the second-order FD | |||
| const float_4 denominatorInv = 0.25 / (deltaBasePhase * deltaBasePhase); | |||
| // pulsewave waveform doesn't have DC even for non 50% duty cycles, but Befaco team would like the option | |||
| // for it to be added back in for hardware compatibility reasons | |||
| const float_4 pulseDCOffset = (!removePulseDC) * 2.f * (0.5f - pw); | |||
| // hard sync | |||
| const float_4 syncMask = syncTrigger[c / 4].process(inputs[SYNC_INPUT].getPolyVoltageSimd<float_4>(c)); | |||
| phase[c / 4] = simd::ifelse(syncMask, 0.5f, phase[c / 4]); | |||
| float_4* osBufferTri = oversampler[TRI_OUTPUT][c / 4].getOSBuffer(); | |||
| float_4* osBufferSaw = oversampler[SAW_OUTPUT][c / 4].getOSBuffer(); | |||
| float_4* osBufferSin = oversampler[SINE_OUTPUT][c / 4].getOSBuffer(); | |||
| float_4* osBufferSquare = oversampler[SQUARE_OUTPUT][c / 4].getOSBuffer(); | |||
| float_4* osBufferEven = oversampler[EVEN_OUTPUT][c / 4].getOSBuffer(); | |||
| for (int i = 0; i < oversamplingRatio; ++i) { | |||
| phase[c / 4] += deltaBasePhase; | |||
| // ensure within [0, 1] | |||
| phase[c / 4] -= simd::floor(phase[c / 4]); | |||
| float_4 phases[3]; // phase as extrapolated to the current and two previous samples | |||
| phases[0] = phase[c / 4] - 2 * deltaBasePhase + simd::ifelse(phase[c / 4] < 2 * deltaBasePhase, 1.f, 0.f); | |||
| phases[1] = phase[c / 4] - deltaBasePhase + simd::ifelse(phase[c / 4] < deltaBasePhase, 1.f, 0.f); | |||
| phases[2] = phase[c / 4]; | |||
| if (outputs[SINE_OUTPUT].isConnected() || outputs[EVEN_OUTPUT].isConnected()) { | |||
| // sin doesn't need PDW | |||
| osBufferSin[i] = -simd::cos(2.0 * M_PI * phase[c / 4]); | |||
| } | |||
| if (outputs[TRI_OUTPUT].isConnected()) { | |||
| const float_4 dpwOrder1 = 1.0 - 2.0 * simd::abs(2 * phase[c / 4] - 1.0); | |||
| const float_4 dpwOrder3 = aliasSuppressedTri(phases) * denominatorInv; | |||
| osBufferTri[i] = simd::ifelse(lowFreqRegime, dpwOrder1, dpwOrder3); | |||
| } | |||
| if (outputs[SAW_OUTPUT].isConnected()) { | |||
| const float_4 dpwOrder1 = 2 * phase[c / 4] - 1.0; | |||
| const float_4 dpwOrder3 = aliasSuppressedSaw(phases) * denominatorInv; | |||
| osBufferSaw[i] = simd::ifelse(lowFreqRegime, dpwOrder1, dpwOrder3); | |||
| } | |||
| if (outputs[SQUARE_OUTPUT].isConnected()) { | |||
| float_4 dpwOrder1 = simd::ifelse(phase[c / 4] < pw, -1.0, +1.0); | |||
| dpwOrder1 -= removePulseDC ? 2.f * (0.5f - pw) : 0.f; | |||
| float_4 saw = aliasSuppressedSaw(phases); | |||
| float_4 sawOffset = aliasSuppressedOffsetSaw(phases, pw); | |||
| float_4 dpwOrder3 = (saw - sawOffset) * denominatorInv + pulseDCOffset; | |||
| osBufferSquare[i] = simd::ifelse(lowFreqRegime, dpwOrder1, dpwOrder3); | |||
| } | |||
| if (outputs[EVEN_OUTPUT].isConnected()) { | |||
| float_4 dpwOrder1 = 4.0 * simd::ifelse(phase[c / 4] < 0.5, phase[c / 4], phase[c / 4] - 0.5) - 1.0; | |||
| float_4 dpwOrder3 = aliasSuppressedDoubleSaw(phases) * denominatorInv; | |||
| float_4 doubleSaw = simd::ifelse(lowFreqRegime, dpwOrder1, dpwOrder3); | |||
| osBufferEven[i] = 0.55 * (doubleSaw + 1.27 * osBufferSin[i]); | |||
| } | |||
| } // end of oversampling loop | |||
| // downsample (if required) | |||
| if (outputs[SINE_OUTPUT].isConnected()) { | |||
| const float_4 outSin = (oversamplingRatio > 1) ? oversampler[SINE_OUTPUT][c / 4].downsample() : osBufferSin[0]; | |||
| outputs[SINE_OUTPUT].setVoltageSimd(5.f * outSin, c); | |||
| } | |||
| if (outputs[TRI_OUTPUT].isConnected()) { | |||
| const float_4 outTri = (oversamplingRatio > 1) ? oversampler[TRI_OUTPUT][c / 4].downsample() : osBufferTri[0]; | |||
| outputs[TRI_OUTPUT].setVoltageSimd(5.f * outTri, c); | |||
| } | |||
| if (outputs[SAW_OUTPUT].isConnected()) { | |||
| const float_4 outSaw = (oversamplingRatio > 1) ? oversampler[SAW_OUTPUT][c / 4].downsample() : osBufferSaw[0]; | |||
| outputs[SAW_OUTPUT].setVoltageSimd(5.f * outSaw, c); | |||
| } | |||
| if (outputs[SQUARE_OUTPUT].isConnected()) { | |||
| const float_4 outSquare = (oversamplingRatio > 1) ? oversampler[SQUARE_OUTPUT][c / 4].downsample() : osBufferSquare[0]; | |||
| outputs[SQUARE_OUTPUT].setVoltageSimd(5.f * outSquare, c); | |||
| } | |||
| if (outputs[EVEN_OUTPUT].isConnected()) { | |||
| const float_4 outEven = (oversamplingRatio > 1) ? oversampler[EVEN_OUTPUT][c / 4].downsample() : osBufferEven[0]; | |||
| outputs[EVEN_OUTPUT].setVoltageSimd(5.f * outEven, c); | |||
| } | |||
| } // end of channels loop | |||
| // Outputs | |||
| outputs[TRI_OUTPUT].setChannels(channels); | |||
| outputs[SINE_OUTPUT].setChannels(channels); | |||
| outputs[EVEN_OUTPUT].setChannels(channels); | |||
| outputs[SAW_OUTPUT].setChannels(channels); | |||
| outputs[SQUARE_OUTPUT].setChannels(channels); | |||
| } | |||
| json_t* dataToJson() override { | |||
| json_t* rootJ = json_object(); | |||
| json_object_set_new(rootJ, "removePulseDC", json_boolean(removePulseDC)); | |||
| json_object_set_new(rootJ, "limitPW", json_boolean(limitPW)); | |||
| json_object_set_new(rootJ, "oversamplingIndex", json_integer(oversampler[0][0].getOversamplingIndex())); | |||
| return rootJ; | |||
| } | |||
| void dataFromJson(json_t* rootJ) override { | |||
| json_t* pulseDCJ = json_object_get(rootJ, "removePulseDC"); | |||
| if (pulseDCJ) { | |||
| removePulseDC = json_boolean_value(pulseDCJ); | |||
| } | |||
| json_t* limitPWJ = json_object_get(rootJ, "limitPW"); | |||
| if (limitPWJ) { | |||
| limitPW = json_boolean_value(limitPWJ); | |||
| } | |||
| json_t* oversamplingIndexJ = json_object_get(rootJ, "oversamplingIndex"); | |||
| if (oversamplingIndexJ) { | |||
| oversamplingIndex = json_integer_value(oversamplingIndexJ); | |||
| onSampleRateChange(); | |||
| } | |||
| } | |||
| }; | |||
| struct EvenVCO2Widget : ModuleWidget { | |||
| EvenVCO2Widget(EvenVCO2* module) { | |||
| setModule(module); | |||
| setPanel(APP->window->loadSvg(asset::plugin(pluginInstance, "res/panels/EvenVCObeta.svg"))); | |||
| addChild(createWidget<Knurlie>(Vec(15, 0))); | |||
| addChild(createWidget<Knurlie>(Vec(15, 365))); | |||
| addChild(createWidget<Knurlie>(Vec(15 * 6, 0))); | |||
| addChild(createWidget<Knurlie>(Vec(15 * 6, 365))); | |||
| addParam(createParam<BefacoBigKnob>(Vec(22, 32), module, EvenVCO2::OCTAVE_PARAM)); | |||
| addParam(createParam<BefacoTinyKnob>(Vec(73, 131), module, EvenVCO2::TUNE_PARAM)); | |||
| addParam(createParam<Davies1900hRedKnob>(Vec(16, 230), module, EvenVCO2::PWM_PARAM)); | |||
| addInput(createInput<BefacoInputPort>(Vec(8, 120), module, EvenVCO2::PITCH1_INPUT)); | |||
| addInput(createInput<BefacoInputPort>(Vec(19, 157), module, EvenVCO2::PITCH2_INPUT)); | |||
| addInput(createInput<BefacoInputPort>(Vec(48, 183), module, EvenVCO2::FM_INPUT)); | |||
| addInput(createInput<BefacoInputPort>(Vec(86, 189), module, EvenVCO2::SYNC_INPUT)); | |||
| addInput(createInput<BefacoInputPort>(Vec(72, 236), module, EvenVCO2::PWM_INPUT)); | |||
| addOutput(createOutput<BefacoOutputPort>(Vec(10, 283), module, EvenVCO2::TRI_OUTPUT)); | |||
| addOutput(createOutput<BefacoOutputPort>(Vec(87, 283), module, EvenVCO2::SINE_OUTPUT)); | |||
| addOutput(createOutput<BefacoOutputPort>(Vec(48, 306), module, EvenVCO2::EVEN_OUTPUT)); | |||
| addOutput(createOutput<BefacoOutputPort>(Vec(10, 327), module, EvenVCO2::SAW_OUTPUT)); | |||
| addOutput(createOutput<BefacoOutputPort>(Vec(87, 327), module, EvenVCO2::SQUARE_OUTPUT)); | |||
| } | |||
| void appendContextMenu(Menu* menu) override { | |||
| EvenVCO2* module = dynamic_cast<EvenVCO2*>(this->module); | |||
| assert(module); | |||
| menu->addChild(new MenuSeparator()); | |||
| menu->addChild(createSubmenuItem("Hardware compatibility", "", | |||
| [ = ](Menu * menu) { | |||
| menu->addChild(createBoolPtrMenuItem("Remove DC from pulse", "", &module->removePulseDC)); | |||
| menu->addChild(createBoolPtrMenuItem("Limit pulsewidth (5\%-95\%)", "", &module->limitPW)); | |||
| } | |||
| )); | |||
| menu->addChild(createIndexSubmenuItem("Oversampling", | |||
| {"Off", "x2", "x4", "x8"}, | |||
| [ = ]() { | |||
| return module->oversamplingIndex; | |||
| }, | |||
| [ = ](int mode) { | |||
| module->oversamplingIndex = mode; | |||
| module->onSampleRateChange(); | |||
| } | |||
| )); | |||
| } | |||
| }; | |||
| Model* modelEvenVCO2 = createModel<EvenVCO2, EvenVCO2Widget>("EvenVCO2"); | |||
| @@ -0,0 +1,344 @@ | |||
| #include "plugin.hpp" | |||
| float aliasSuppressedSaw(const float* phases, float pw) { | |||
| float sawBuffer[3]; | |||
| for (int i = 0; i < 3; ++i) { | |||
| float p = 2 * phases[i] - 1.0; // range -1 to +1 | |||
| float pwp = p + 2 * pw; // phase after pw (pw in [0, 1]) | |||
| pwp += simd::ifelse(pwp > 1, -2, simd::ifelse(pwp < -1, +2, 0)); // modulo on [-1, +1] | |||
| sawBuffer[i] = (pwp * pwp * pwp - pwp) / 6.0; // eq 11 | |||
| } | |||
| return (sawBuffer[0] - 2.0 * sawBuffer[1] + sawBuffer[2]); | |||
| } | |||
| float aliasSuppressedOffsetSaw(const float* phases, float pw) { | |||
| float sawOffsetBuff[3]; | |||
| for (int i = 0; i < 3; ++i) { | |||
| float pwp = 2 * phases[i] - 2 * pw; // range -1 to +1 | |||
| pwp += simd::ifelse(pwp > 1, -2, 0); // modulo on [-1, +1] | |||
| sawOffsetBuff[i] = (pwp * pwp * pwp - pwp) / 6.0; // eq 11 | |||
| } | |||
| return (sawOffsetBuff[0] - 2.0 * sawOffsetBuff[1] + sawOffsetBuff[2]); | |||
| } | |||
| struct Octaves : Module { | |||
| enum ParamId { | |||
| PWM_CV_PARAM, | |||
| OCTAVE_PARAM, | |||
| TUNE_PARAM, | |||
| PWM_PARAM, | |||
| RANGE_PARAM, | |||
| GAIN_01F_PARAM, | |||
| GAIN_02F_PARAM, | |||
| GAIN_04F_PARAM, | |||
| GAIN_08F_PARAM, | |||
| GAIN_16F_PARAM, | |||
| GAIN_32F_PARAM, | |||
| PARAMS_LEN | |||
| }; | |||
| enum InputId { | |||
| VOCT1_INPUT, | |||
| VOCT2_INPUT, | |||
| SYNC_INPUT, | |||
| PWM_INPUT, | |||
| GAIN_01F_INPUT, | |||
| GAIN_02F_INPUT, | |||
| GAIN_04F_INPUT, | |||
| GAIN_08F_INPUT, | |||
| GAIN_16F_INPUT, | |||
| GAIN_32F_INPUT, | |||
| INPUTS_LEN | |||
| }; | |||
| enum OutputId { | |||
| OUT_01F_OUTPUT, | |||
| OUT_02F_OUTPUT, | |||
| OUT_04F_OUTPUT, | |||
| OUT_08F_OUTPUT, | |||
| OUT_16F_OUTPUT, | |||
| OUT_32F_OUTPUT, | |||
| OUT_OUTPUT, | |||
| OUT2_OUTPUT, | |||
| OUT_01F_OUTPUT_ALT, | |||
| OUT_02F_OUTPUT_ALT, | |||
| OUT_04F_OUTPUT_ALT, | |||
| OUT_08F_OUTPUT_ALT, | |||
| OUT_16F_OUTPUT_ALT, | |||
| OUT_32F_OUTPUT_ALT, | |||
| OUTPUTS_LEN | |||
| }; | |||
| enum LightId { | |||
| LIGHTS_LEN | |||
| }; | |||
| bool limitPW = true; | |||
| bool removePulseDC = false; | |||
| int oversamplingIndex = 0; | |||
| Octaves() { | |||
| config(PARAMS_LEN, INPUTS_LEN, OUTPUTS_LEN, LIGHTS_LEN); | |||
| configParam(PWM_CV_PARAM, 0.f, 1.f, 1.f, "PWM CV attenuater"); | |||
| auto octParam = configSwitch(OCTAVE_PARAM, 0.f, 6.f, 4.f, "Octave", {"C1", "C2", "C3", "C4", "C5", "C6", "C7"}); | |||
| octParam->snapEnabled = true; | |||
| configParam(TUNE_PARAM, -1.f, 1.f, 0.f, "Tune"); | |||
| configParam(PWM_PARAM, 0.5f, 0.f, 0.5f, "PWM"); | |||
| auto rangeParam = configSwitch(RANGE_PARAM, 0.f, 2.f, 0.f, "Range", {"VCO: Full", "VCO: Octave", "VCO: Semitone"}); | |||
| rangeParam->snapEnabled = true; | |||
| configParam(GAIN_01F_PARAM, 0.f, 1.f, 0.f, "Gain Fundamental"); | |||
| configParam(GAIN_02F_PARAM, 0.f, 1.f, 0.f, "Gain x2 Fundamental"); | |||
| configParam(GAIN_04F_PARAM, 0.f, 1.f, 0.f, "Gain x4 Fundamental"); | |||
| configParam(GAIN_08F_PARAM, 0.f, 1.f, 0.f, "Gain x8 Fundamental"); | |||
| configParam(GAIN_16F_PARAM, 0.f, 1.f, 0.f, "Gain x16 Fundamental"); | |||
| configParam(GAIN_32F_PARAM, 0.f, 1.f, 0.f, "Gain x32 Fundamental"); | |||
| configInput(VOCT1_INPUT, "V/Octave 1"); | |||
| configInput(VOCT2_INPUT, "V/Octave 2"); | |||
| configInput(SYNC_INPUT, "Sync"); | |||
| configInput(PWM_INPUT, "PWM"); | |||
| configInput(GAIN_01F_INPUT, "Gain x1F CV"); | |||
| configInput(GAIN_02F_INPUT, "Gain x1F CV"); | |||
| configInput(GAIN_04F_INPUT, "Gain x1F CV"); | |||
| configInput(GAIN_08F_INPUT, "Gain x1F CV"); | |||
| configInput(GAIN_16F_INPUT, "Gain x1F CV"); | |||
| configInput(GAIN_32F_INPUT, "Gain x1F CV"); | |||
| configOutput(OUT_01F_OUTPUT, "x1F"); | |||
| configOutput(OUT_02F_OUTPUT, "x2F"); | |||
| configOutput(OUT_04F_OUTPUT, "x4F"); | |||
| configOutput(OUT_08F_OUTPUT, "x8F"); | |||
| configOutput(OUT_16F_OUTPUT, "x16F"); | |||
| configOutput(OUT_32F_OUTPUT, "x32F"); | |||
| configOutput(OUT_OUTPUT, "debug"); | |||
| } | |||
| float phase = 0.f; | |||
| float phases[3]; | |||
| bool forceNaive = false; | |||
| void process(const ProcessArgs& args) override { | |||
| float pitch = params[TUNE_PARAM].getValue() + inputs[VOCT1_INPUT].getVoltage() + inputs[VOCT2_INPUT].getVoltage(); | |||
| pitch += params[OCTAVE_PARAM].getValue() - 3; | |||
| float freq = dsp::FREQ_C4 * dsp::exp2_taylor5(pitch); | |||
| // -1 to +1 | |||
| float pwmCV = params[PWM_CV_PARAM].getValue() * clamp(inputs[PWM_INPUT].getVoltage() / 10.f, -1.f, 1.f); | |||
| const float pulseWidthLimit = limitPW ? 0.05f : 0.0f; | |||
| // pwm in [-0.25 : +0.25] | |||
| float pwm = clamp(0.5 - params[PWM_PARAM].getValue() + 0.5 * pwmCV, -0.5f + pulseWidthLimit, 0.5f - pulseWidthLimit); | |||
| pwm /= 2.0; | |||
| float deltaPhase = freq * args.sampleTime; | |||
| phase += deltaPhase; | |||
| phase -= std::floor(phase); | |||
| float sum = 0.f; | |||
| float sumNaive = 0.f; | |||
| for (int c = 0; c < 6; c++) { | |||
| // derive phases for higher octaves from base phase (this keeps things in sync!) | |||
| const float n = (float)(1 << c); | |||
| // this is on [0, 1] | |||
| const float effectivePhaseRaw = n * std::fmod(phase, 1 / n); | |||
| // this is on [0, 1], and offset in time by 0.25 | |||
| const float effectivePhase = std::fmod(effectivePhaseRaw + 0.25, 1); | |||
| const float effectiveDeltaPhase = deltaPhase * n; | |||
| const float gainCV = clamp(inputs[GAIN_01F_INPUT + c].getNormalVoltage(10.f) / 10.f, 0.f, 1.0f); | |||
| const float gain = params[GAIN_01F_PARAM + c].getValue() * gainCV; | |||
| // floating point arithmetic doesn't work well at low frequencies, specifically because the finite difference denominator | |||
| // becomes tiny - we check for that scenario and use naive / 1st order waveforms in that frequency regime (as aliasing isn't | |||
| // a problem there). With no oversampling, at 44100Hz, the threshold frequency is 44.1Hz. | |||
| const bool lowFreqRegime = forceNaive; //effectiveDeltaPhase < 1e-3 || forceNaive; | |||
| //float waveTri = 1.0 - 2.0 * std::abs(2.f * effectivePhase - 1.0); | |||
| // float dpwOrder1 = (waveTri > 2 * pwm - 1) ? 1.0 : -1.0; | |||
| float dpwOrder1 = gain * (effectivePhaseRaw > pwm + 0.25 && effectivePhaseRaw < 0.75 - pwm ? -1.0 : +1.0); | |||
| dpwOrder1 -= removePulseDC ? 2.f * (0.5f - pwm) : 0.f; | |||
| // dpwOrder1 = waveTri * gain; | |||
| sumNaive += dpwOrder1; | |||
| outputs[OUT_01F_OUTPUT_ALT + c].setVoltage(dpwOrder1); | |||
| float outForOctave = dpwOrder1; | |||
| if (!lowFreqRegime) { | |||
| phases[0] = effectivePhase - 2 * effectiveDeltaPhase + (effectivePhase < 2 * effectiveDeltaPhase ? 1.f : 0.f); | |||
| phases[1] = effectivePhase - 1 * effectiveDeltaPhase + (effectivePhase < 1 * effectiveDeltaPhase ? 1.f : 0.f); | |||
| phases[2] = effectivePhase; | |||
| float saw = aliasSuppressedSaw(phases, pwm); | |||
| float sawOffset = aliasSuppressedOffsetSaw(phases, pwm); | |||
| float denominatorInv = 0.25 / (effectiveDeltaPhase * effectiveDeltaPhase); | |||
| float dpwOrder3 = gain * (sawOffset - saw) * denominatorInv; | |||
| const float pulseDCOffset = (!removePulseDC) * 4.f * pwm * gain; | |||
| dpwOrder3 += pulseDCOffset; | |||
| outForOctave = dpwOrder3; | |||
| } | |||
| sum += outForOctave; | |||
| sum = clamp(sum, -1.f, 1.f); | |||
| if (outputs[OUT_01F_OUTPUT + c].isConnected()) { | |||
| outputs[OUT_01F_OUTPUT + c].setVoltage(5 * sum); | |||
| sum = 0.f; | |||
| } | |||
| if (c == 0) { | |||
| outputs[OUT_OUTPUT].setVoltage(effectivePhase); | |||
| float saw = aliasSuppressedSaw(phases, 2*pwm); | |||
| float sawOffset = aliasSuppressedOffsetSaw(phases, 2*pwm); | |||
| float denominatorInv = 0.25 / (effectiveDeltaPhase * effectiveDeltaPhase); | |||
| float dpwOrder3_ = gain * (-saw) * denominatorInv; | |||
| outputs[OUT2_OUTPUT].setVoltage(dpwOrder3_); | |||
| } | |||
| } | |||
| //outputs[OUT_OUTPUT].setVoltage(sum); | |||
| //outputs[OUT2_OUTPUT].setVoltage(phase > 0.5 ? +5 : -5); | |||
| } | |||
| json_t* dataToJson() override { | |||
| json_t* rootJ = json_object(); | |||
| json_object_set_new(rootJ, "removePulseDC", json_boolean(removePulseDC)); | |||
| json_object_set_new(rootJ, "limitPW", json_boolean(limitPW)); | |||
| json_object_set_new(rootJ, "forceNaive", json_boolean(forceNaive)); | |||
| // TODO: | |||
| // json_object_set_new(rootJ, "oversamplingIndex", json_integer(oversampler[0].getOversamplingIndex())); | |||
| return rootJ; | |||
| } | |||
| void dataFromJson(json_t* rootJ) override { | |||
| json_t* removePulseDCJ = json_object_get(rootJ, "removePulseDC"); | |||
| if (removePulseDCJ) { | |||
| removePulseDC = json_boolean_value(removePulseDCJ); | |||
| } | |||
| json_t* limitPWJ = json_object_get(rootJ, "limitPW"); | |||
| if (limitPWJ) { | |||
| limitPW = json_boolean_value(limitPWJ); | |||
| } | |||
| json_t* forceNaiveJ = json_object_get(rootJ, "forceNaive"); | |||
| if (forceNaiveJ) { | |||
| forceNaive = json_boolean_value(forceNaiveJ); | |||
| } | |||
| json_t* oversamplingIndexJ = json_object_get(rootJ, "oversamplingIndex"); | |||
| if (oversamplingIndexJ) { | |||
| oversamplingIndex = json_integer_value(oversamplingIndexJ); | |||
| onSampleRateChange(); | |||
| } | |||
| } | |||
| }; | |||
| struct OctavesWidget : ModuleWidget { | |||
| OctavesWidget(Octaves* module) { | |||
| setModule(module); | |||
| setPanel(createPanel(asset::plugin(pluginInstance, "res/panels/Octaves.svg"))); | |||
| addChild(createWidget<Knurlie>(Vec(RACK_GRID_WIDTH, 0))); | |||
| addChild(createWidget<Knurlie>(Vec(box.size.x - 2 * RACK_GRID_WIDTH, 0))); | |||
| addChild(createWidget<Knurlie>(Vec(RACK_GRID_WIDTH, RACK_GRID_HEIGHT - RACK_GRID_WIDTH))); | |||
| addChild(createWidget<Knurlie>(Vec(box.size.x - 2 * RACK_GRID_WIDTH, RACK_GRID_HEIGHT - RACK_GRID_WIDTH))); | |||
| addParam(createParamCentered<BefacoTinyKnobLightGrey>(mm2px(Vec(52.138, 15.037)), module, Octaves::PWM_CV_PARAM)); | |||
| addParam(createParam<CKSSVert7>(mm2px(Vec(22.171, 30.214)), module, Octaves::OCTAVE_PARAM)); | |||
| addParam(createParamCentered<BefacoTinyKnobLightGrey>(mm2px(Vec(10.264, 33.007)), module, Octaves::TUNE_PARAM)); | |||
| addParam(createParamCentered<Davies1900hLargeRedKnob>(mm2px(Vec(45.384, 40.528)), module, Octaves::PWM_PARAM)); | |||
| addParam(createParam<CKSSThreeHorizontal>(mm2px(Vec(6.023, 48.937)), module, Octaves::RANGE_PARAM)); | |||
| addParam(createParam<BefacoSlidePotSmall>(mm2px(Vec(2.9830, 60.342)), module, Octaves::GAIN_01F_PARAM)); | |||
| addParam(createParam<BefacoSlidePotSmall>(mm2px(Vec(12.967, 60.342)), module, Octaves::GAIN_02F_PARAM)); | |||
| addParam(createParam<BefacoSlidePotSmall>(mm2px(Vec(22.951, 60.342)), module, Octaves::GAIN_04F_PARAM)); | |||
| addParam(createParam<BefacoSlidePotSmall>(mm2px(Vec(32.936, 60.342)), module, Octaves::GAIN_08F_PARAM)); | |||
| addParam(createParam<BefacoSlidePotSmall>(mm2px(Vec(42.920, 60.342)), module, Octaves::GAIN_16F_PARAM)); | |||
| addParam(createParam<BefacoSlidePotSmall>(mm2px(Vec(52.905, 60.342)), module, Octaves::GAIN_32F_PARAM)); | |||
| addInput(createInputCentered<BefacoInputPort>(mm2px(Vec(5.247, 15.181)), module, Octaves::VOCT1_INPUT)); | |||
| addInput(createInputCentered<BefacoInputPort>(mm2px(Vec(15.282, 15.181)), module, Octaves::VOCT2_INPUT)); | |||
| addInput(createInputCentered<BefacoInputPort>(mm2px(Vec(25.316, 15.181)), module, Octaves::SYNC_INPUT)); | |||
| addInput(createInputCentered<BefacoInputPort>(mm2px(Vec(37.092, 15.135)), module, Octaves::PWM_INPUT)); | |||
| addInput(createInputCentered<BefacoInputPort>(mm2px(Vec(5.247, 100.492)), module, Octaves::GAIN_01F_INPUT)); | |||
| addInput(createInputCentered<BefacoInputPort>(mm2px(Vec(15.282, 100.492)), module, Octaves::GAIN_02F_INPUT)); | |||
| addInput(createInputCentered<BefacoInputPort>(mm2px(Vec(25.316, 100.492)), module, Octaves::GAIN_04F_INPUT)); | |||
| addInput(createInputCentered<BefacoInputPort>(mm2px(Vec(35.35, 100.492)), module, Octaves::GAIN_08F_INPUT)); | |||
| addInput(createInputCentered<BefacoInputPort>(mm2px(Vec(45.384, 100.492)), module, Octaves::GAIN_16F_INPUT)); | |||
| addInput(createInputCentered<BefacoInputPort>(mm2px(Vec(55.418, 100.492)), module, Octaves::GAIN_32F_INPUT)); | |||
| addOutput(createOutputCentered<BefacoOutputPort>(mm2px(Vec(5.247, 113.508)), module, Octaves::OUT_01F_OUTPUT)); | |||
| addOutput(createOutputCentered<BefacoOutputPort>(mm2px(Vec(15.282, 113.508)), module, Octaves::OUT_02F_OUTPUT)); | |||
| addOutput(createOutputCentered<BefacoOutputPort>(mm2px(Vec(25.316, 113.508)), module, Octaves::OUT_04F_OUTPUT)); | |||
| addOutput(createOutputCentered<BefacoOutputPort>(mm2px(Vec(35.35, 113.508)), module, Octaves::OUT_08F_OUTPUT)); | |||
| addOutput(createOutputCentered<BefacoOutputPort>(mm2px(Vec(45.384, 113.508)), module, Octaves::OUT_16F_OUTPUT)); | |||
| addOutput(createOutputCentered<BefacoOutputPort>(mm2px(Vec(55.418, 113.508)), module, Octaves::OUT_32F_OUTPUT)); | |||
| addOutput(createOutputCentered<BefacoOutputPort>(mm2px(Vec(25.316, 106.508)), module, Octaves::OUT_OUTPUT)); | |||
| addOutput(createOutputCentered<BefacoOutputPort>(mm2px(Vec(35.316, 106.508)), module, Octaves::OUT2_OUTPUT)); | |||
| addOutput(createOutputCentered<BefacoOutputPort>(mm2px(Vec(5.247, 120.508)), module, Octaves::OUT_01F_OUTPUT_ALT)); | |||
| addOutput(createOutputCentered<BefacoOutputPort>(mm2px(Vec(15.282, 120.508)), module, Octaves::OUT_02F_OUTPUT_ALT)); | |||
| addOutput(createOutputCentered<BefacoOutputPort>(mm2px(Vec(25.316, 120.508)), module, Octaves::OUT_04F_OUTPUT_ALT)); | |||
| addOutput(createOutputCentered<BefacoOutputPort>(mm2px(Vec(35.35, 120.508)), module, Octaves::OUT_08F_OUTPUT_ALT)); | |||
| addOutput(createOutputCentered<BefacoOutputPort>(mm2px(Vec(45.384, 120.508)), module, Octaves::OUT_16F_OUTPUT_ALT)); | |||
| addOutput(createOutputCentered<BefacoOutputPort>(mm2px(Vec(55.418, 120.508)), module, Octaves::OUT_32F_OUTPUT_ALT)); | |||
| } | |||
| void appendContextMenu(Menu* menu) override { | |||
| Octaves* module = dynamic_cast<Octaves*>(this->module); | |||
| assert(module); | |||
| menu->addChild(new MenuSeparator()); | |||
| menu->addChild(createSubmenuItem("Hardware compatibility", "", | |||
| [ = ](Menu * menu) { | |||
| menu->addChild(createBoolPtrMenuItem("Limit pulsewidth (5\%-95\%)", "", &module->limitPW)); | |||
| menu->addChild(createBoolPtrMenuItem("Remove pulse DC", "", &module->removePulseDC)); | |||
| } | |||
| )); | |||
| menu->addChild(createIndexSubmenuItem("Oversampling", | |||
| {"Off", "x2", "x4", "x8"}, | |||
| [ = ]() { | |||
| return module->oversamplingIndex; | |||
| }, | |||
| [ = ](int mode) { | |||
| module->oversamplingIndex = mode; | |||
| module->onSampleRateChange(); | |||
| } | |||
| )); | |||
| menu->addChild(createBoolPtrMenuItem("Force naive waveforms", "", &module->forceNaive)); | |||
| } | |||
| }; | |||
| Model* modelOctaves = createModel<Octaves, OctavesWidget>("Octaves"); | |||
| @@ -7,7 +7,6 @@ void init(rack::Plugin *p) { | |||
| pluginInstance = p; | |||
| p->addModel(modelEvenVCO); | |||
| p->addModel(modelEvenVCO2); | |||
| p->addModel(modelRampage); | |||
| p->addModel(modelABC); | |||
| p->addModel(modelSpringReverb); | |||
| @@ -31,4 +30,5 @@ void init(rack::Plugin *p) { | |||
| p->addModel(modelBurst); | |||
| p->addModel(modelMidiThing); | |||
| p->addModel(modelVoltio); | |||
| p->addModel(modelOctaves); | |||
| } | |||
| @@ -8,7 +8,6 @@ using namespace rack; | |||
| extern Plugin* pluginInstance; | |||
| extern Model* modelEvenVCO; | |||
| extern Model* modelEvenVCO2; | |||
| extern Model* modelRampage; | |||
| extern Model* modelABC; | |||
| extern Model* modelSpringReverb; | |||
| @@ -32,6 +31,7 @@ extern Model* modelMotionMTR; | |||
| extern Model* modelBurst; | |||
| extern Model* modelMidiThing; | |||
| extern Model* modelVoltio; | |||
| extern Model* modelOctaves; | |||
| struct Knurlie : SvgScrew { | |||
| Knurlie() { | |||