#include "plugin.hpp" struct BefacoADSREnvelope { enum Stage { STAGE_OFF, STAGE_ATTACK, STAGE_DECAY, STAGE_SUSTAIN, STAGE_RELEASE }; Stage stage = STAGE_OFF; float env = 0.f; float releaseValue; float timeInCurrentStage = 0.f; float attackTime = 0.1, decayTime = 0.1, releaseTime = 0.1; float attackShape = 1.0, decayShape = 1.0, releaseShape = 1.0; float sustainLevel; BefacoADSREnvelope() { }; void retrigger() { stage = STAGE_ATTACK; // get the linear value of the envelope timeInCurrentStage = attackTime * std::pow(env, 1.0f / attackShape); } void processTransitionsGateMode(const bool& gateHeld) { if (gateHeld) { // calculate stage transitions switch (stage) { case STAGE_OFF: { env = 0.0f; timeInCurrentStage = 0.f; stage = STAGE_ATTACK; break; } case STAGE_ATTACK: { if (env >= 1.f) { timeInCurrentStage = 0.f; stage = STAGE_DECAY; } break; } case STAGE_DECAY: { if (timeInCurrentStage >= decayTime) { timeInCurrentStage = 0.f; stage = STAGE_SUSTAIN; } break; } case STAGE_SUSTAIN: { break; } case STAGE_RELEASE: { stage = STAGE_ATTACK; timeInCurrentStage = attackTime * env; break; } } } else { if (stage == STAGE_ATTACK || stage == STAGE_DECAY || stage == STAGE_SUSTAIN) { timeInCurrentStage = 0.f; stage = STAGE_RELEASE; releaseValue = env; } else if (stage == STAGE_RELEASE) { if (timeInCurrentStage >= releaseTime) { stage = STAGE_OFF; timeInCurrentStage = 0.f; } } } } void processTransitionsTriggerMode(const bool& gateHeld) { // calculate stage transitions switch (stage) { case STAGE_ATTACK: { if (env >= 1.f) { timeInCurrentStage = 0.f; if (gateHeld) { stage = STAGE_DECAY; } else { stage = STAGE_RELEASE; releaseValue = 1.f; } } break; } case STAGE_DECAY: { if (timeInCurrentStage >= decayTime) { timeInCurrentStage = 0.f; if (gateHeld) { stage = STAGE_SUSTAIN; } else { stage = STAGE_RELEASE; releaseValue = env; } } break; } case STAGE_OFF: case STAGE_RELEASE: case STAGE_SUSTAIN: { break; } } if (!gateHeld) { if (stage == STAGE_DECAY || stage == STAGE_SUSTAIN) { timeInCurrentStage = 0.f; stage = STAGE_RELEASE; releaseValue = env; } else if (stage == STAGE_RELEASE) { if (timeInCurrentStage >= releaseTime) { stage = STAGE_OFF; timeInCurrentStage = 0.f; } } } } void evolveEnvelope(const float& sampleTime) { switch (stage) { case STAGE_OFF: { env = 0.0f; break; } case STAGE_ATTACK: { timeInCurrentStage += sampleTime; env = std::min(timeInCurrentStage / attackTime, 1.f); env = std::pow(env, attackShape); break; } case STAGE_DECAY: { timeInCurrentStage += sampleTime; env = std::pow(1.f - std::min(1.f, timeInCurrentStage / decayTime), decayShape); env = sustainLevel + (1.f - sustainLevel) * env; break; } case STAGE_SUSTAIN: { env = sustainLevel; break; } case STAGE_RELEASE: { timeInCurrentStage += sampleTime; env = std::min(1.0f, timeInCurrentStage / releaseTime); env = releaseValue * std::pow(1.0f - env, releaseShape); break; } } } void process(const float& sampleTime, const bool& gateHeld, const bool& triggerMode) { if (triggerMode) { processTransitionsTriggerMode(gateHeld); } else { processTransitionsGateMode(gateHeld); } evolveEnvelope(sampleTime); } }; struct ADSR : Module { enum ParamIds { TRIGG_GATE_TOGGLE_PARAM, MANUAL_TRIGGER_PARAM, SHAPE_PARAM, ATTACK_PARAM, DECAY_PARAM, SUSTAIN_PARAM, RELEASE_PARAM, NUM_PARAMS }; enum InputIds { TRIGGER_INPUT, CV_ATTACK_INPUT, CV_DECAY_INPUT, CV_SUSTAIN_INPUT, CV_RELEASE_INPUT, NUM_INPUTS }; enum OutputIds { OUT_OUTPUT, STAGE_ATTACK_OUTPUT, STAGE_DECAY_OUTPUT, STAGE_SUSTAIN_OUTPUT, STAGE_RELEASE_OUTPUT, NUM_OUTPUTS }; enum LightIds { LED_LIGHT, LED_ATTACK_LIGHT, LED_DECAY_LIGHT, LED_SUSTAIN_LIGHT, LED_RELEASE_LIGHT, NUM_LIGHTS }; enum EnvelopeMode { GATE_MODE, TRIGGER_MODE }; BefacoADSREnvelope envelope; dsp::SchmittTrigger gateTrigger; dsp::ClockDivider cvDivider; float shape; static constexpr float minStageTime = 0.003f; // in seconds static constexpr float maxStageTime = 10.f; // in seconds // given a value from the slider and/or cv (rescaled to range 0 to 1), transform into the appropriate time in seconds static float convertCVToTimeInSeconds(float cv) { return minStageTime * std::pow(maxStageTime / minStageTime, cv); } ADSR() { config(NUM_PARAMS, NUM_INPUTS, NUM_OUTPUTS, NUM_LIGHTS); configSwitch(TRIGG_GATE_TOGGLE_PARAM, GATE_MODE, TRIGGER_MODE, GATE_MODE, "Mode", {"Gate", "Trigger"}); configButton(MANUAL_TRIGGER_PARAM, "Trigger envelope"); configParam(SHAPE_PARAM, 0.f, 1.f, 0.f, "Envelope shape"); configParam(ATTACK_PARAM, 0.f, 1.f, 0.4f, "Attack time", "s", maxStageTime / minStageTime, minStageTime); configParam(DECAY_PARAM, 0.f, 1.f, 0.4f, "Decay time", "s", maxStageTime / minStageTime, minStageTime); configParam(SUSTAIN_PARAM, 0.f, 1.f, 0.5f, "Sustain level", "%", 0.f, 100.f); configParam(RELEASE_PARAM, 0.f, 1.f, 0.4f, "Release time", "s", maxStageTime / minStageTime, minStageTime); configInput(TRIGGER_INPUT, "Trigger"); configInput(CV_ATTACK_INPUT, "Attack CV"); configInput(CV_DECAY_INPUT, "Decay CV"); configInput(CV_SUSTAIN_INPUT, "Sustain CV"); configInput(CV_RELEASE_INPUT, "Release CV"); configOutput(OUT_OUTPUT, "Envelope"); configOutput(STAGE_ATTACK_OUTPUT, "Attack stage"); configOutput(STAGE_DECAY_OUTPUT, "Decay stage"); configOutput(STAGE_SUSTAIN_OUTPUT, "Sustain stage"); configOutput(STAGE_RELEASE_OUTPUT, "Release stage"); cvDivider.setDivision(16); } void process(const ProcessArgs& args) override { if (cvDivider.process()) { shape = params[SHAPE_PARAM].getValue(); envelope.decayShape = 1.f + shape; envelope.attackShape = 1.f - shape / 2.f; envelope.releaseShape = 1.f + shape; const float attackCV = clamp(params[ATTACK_PARAM].getValue() + inputs[CV_ATTACK_INPUT].getVoltage() / 10.f, 0.f, 1.f); envelope.attackTime = convertCVToTimeInSeconds(attackCV); const float decayCV = clamp(params[DECAY_PARAM].getValue() + inputs[CV_DECAY_INPUT].getVoltage() / 10.f, 0.f, 1.f); envelope.decayTime = convertCVToTimeInSeconds(decayCV); const float sustainCV = clamp(params[SUSTAIN_PARAM].getValue() + inputs[CV_SUSTAIN_INPUT].getVoltage() / 10.f, 0.f, 1.f); envelope.sustainLevel = sustainCV; const float releaseCV = clamp(params[RELEASE_PARAM].getValue() + inputs[CV_RELEASE_INPUT].getVoltage() / 10.f, 0.f, 1.f); envelope.releaseTime = convertCVToTimeInSeconds(releaseCV); } const bool triggered = gateTrigger.process(rescale(params[MANUAL_TRIGGER_PARAM].getValue() * 10.f + inputs[TRIGGER_INPUT].getVoltage(), 0.1f, 2.f, 0.f, 1.f)); const bool gateOn = gateTrigger.isHigh() || params[MANUAL_TRIGGER_PARAM].getValue(); const bool triggerMode = params[TRIGG_GATE_TOGGLE_PARAM].getValue() == 1; if (triggerMode) { if (triggered) { envelope.retrigger(); } } envelope.process(args.sampleTime, gateOn, triggerMode); outputs[OUT_OUTPUT].setVoltage(envelope.env * 10.f); outputs[STAGE_ATTACK_OUTPUT].setVoltage(10.f * (envelope.stage == BefacoADSREnvelope::STAGE_ATTACK)); outputs[STAGE_DECAY_OUTPUT].setVoltage(10.f * (envelope.stage == BefacoADSREnvelope::STAGE_DECAY)); outputs[STAGE_SUSTAIN_OUTPUT].setVoltage(10.f * (envelope.stage == BefacoADSREnvelope::STAGE_SUSTAIN)); outputs[STAGE_RELEASE_OUTPUT].setVoltage(10.f * (envelope.stage == BefacoADSREnvelope::STAGE_RELEASE)); lights[LED_ATTACK_LIGHT].setBrightness((envelope.stage == BefacoADSREnvelope::STAGE_ATTACK)); lights[LED_DECAY_LIGHT].setBrightness((envelope.stage == BefacoADSREnvelope::STAGE_DECAY)); lights[LED_SUSTAIN_LIGHT].setBrightness((envelope.stage == BefacoADSREnvelope::STAGE_SUSTAIN)); lights[LED_RELEASE_LIGHT].setBrightness((envelope.stage == BefacoADSREnvelope::STAGE_RELEASE)); lights[LED_LIGHT].setBrightness((float) gateOn); } }; struct ADSRWidget : ModuleWidget { ADSRWidget(ADSR* module) { setModule(module); setPanel(APP->window->loadSvg(asset::plugin(pluginInstance, "res/panels/ADSR.svg"))); addChild(createWidget(Vec(RACK_GRID_WIDTH, 0))); addChild(createWidget(Vec(box.size.x - 2 * RACK_GRID_WIDTH, 0))); addChild(createWidget(Vec(RACK_GRID_WIDTH, RACK_GRID_HEIGHT - RACK_GRID_WIDTH))); addChild(createWidget(Vec(box.size.x - 2 * RACK_GRID_WIDTH, RACK_GRID_HEIGHT - RACK_GRID_WIDTH))); addParam(createParamCentered(mm2px(Vec(20.263, 17.128)), module, ADSR::TRIGG_GATE_TOGGLE_PARAM)); addParam(createParamCentered(mm2px(Vec(11.581, 32.473)), module, ADSR::MANUAL_TRIGGER_PARAM)); addParam(createParamCentered(mm2px(Vec(29.063, 32.573)), module, ADSR::SHAPE_PARAM)); addParam(createParam(mm2px(Vec(2.294, 45.632)), module, ADSR::ATTACK_PARAM)); addParam(createParam(mm2px(Vec(12.422, 45.632)), module, ADSR::DECAY_PARAM)); addParam(createParam(mm2px(Vec(22.551, 45.632)), module, ADSR::SUSTAIN_PARAM)); addParam(createParam(mm2px(Vec(32.68, 45.632)), module, ADSR::RELEASE_PARAM)); addInput(createInputCentered(mm2px(Vec(6.841, 15.5)), module, ADSR::TRIGGER_INPUT)); addInput(createInputCentered(mm2px(Vec(5.022, 113.506)), module, ADSR::CV_ATTACK_INPUT)); addInput(createInputCentered(mm2px(Vec(15.195, 113.506)), module, ADSR::CV_DECAY_INPUT)); addInput(createInputCentered(mm2px(Vec(25.368, 113.506)), module, ADSR::CV_SUSTAIN_INPUT)); addInput(createInputCentered(mm2px(Vec(35.541, 113.506)), module, ADSR::CV_RELEASE_INPUT)); addOutput(createOutputCentered(mm2px(Vec(33.721, 15.479)), module, ADSR::OUT_OUTPUT)); addOutput(createOutputCentered(mm2px(Vec(5.022, 100.858)), module, ADSR::STAGE_ATTACK_OUTPUT)); addOutput(createOutputCentered(mm2px(Vec(15.195, 100.858)), module, ADSR::STAGE_DECAY_OUTPUT)); addOutput(createOutputCentered(mm2px(Vec(25.368, 100.858)), module, ADSR::STAGE_SUSTAIN_OUTPUT)); addOutput(createOutputCentered(mm2px(Vec(35.541, 100.858)), module, ADSR::STAGE_RELEASE_OUTPUT)); addChild(createLightCentered>(mm2px(Vec(20.254, 40.864)), module, ADSR::LED_LIGHT)); addChild(createLightCentered>(mm2px(Vec(5.001, 92.893)), module, ADSR::LED_ATTACK_LIGHT)); addChild(createLightCentered>(mm2px(Vec(15.174, 92.893)), module, ADSR::LED_DECAY_LIGHT)); addChild(createLightCentered>(mm2px(Vec(25.347, 92.893)), module, ADSR::LED_SUSTAIN_LIGHT)); addChild(createLightCentered>(mm2px(Vec(35.52, 92.893)), module, ADSR::LED_RELEASE_LIGHT)); } }; Model* modelADSR = createModel("ADSR");