Browse Source

Added first pass at VC ADSR

tags/v2.1.0
hemmer 3 years ago
parent
commit
b4c82a401f
4 changed files with 361 additions and 0 deletions
  1. +11
    -0
      plugin.json
  2. +348
    -0
      src/ADSR.cpp
  3. +1
    -0
      src/plugin.cpp
  4. +1
    -0
      src/plugin.hpp

+ 11
- 0
plugin.json View File

@@ -179,6 +179,17 @@
"Polyphonic",
"Quad"
]
},
{
"slug": "ADSR",
"name": "ADSR",
"description": "ADSR envelope generator with gate output on each stage, plus variable shape",
"manualUrl": "https://www.befaco.org/vc-adsr/",
"modularGridUrl": "https://www.modulargrid.net/e/befaco-vc-adsr",
"tags": [
"Envelope generator",
"Hardware clone"
]
}
]
}

+ 348
- 0
src/ADSR.cpp View File

@@ -0,0 +1,348 @@
#include "plugin.hpp"


struct ADSREnvelope {

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;

ADSREnvelope() { };

void retrigger() {
stage = ADSREnvelope::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
};

ADSREnvelope 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
static constexpr float scaleFactor = std::log2(maxStageTime / minStageTime);

// 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) {
float cv2 = cv * cv;
// according to hardware, slider appears to respond roughly as a quartic
return minStageTime + (maxStageTime - minStageTime) * cv2 * cv2;
}

// given a time in seconds, transform into the appropriate CV/slider value (in range 0, 1)
static float convertTimeInSecondsToCV(float timeInSecs) {
// according to hardware, slider appears to respond roughly as a quartic
return std::pow((timeInSecs - minStageTime) / (maxStageTime - minStageTime), 0.25f);
}

struct StageTimeParam : ParamQuantity {
std::string getDisplayValueString() override {
return string::f("%.3f", convertCVToTimeInSeconds(getValue()));
}

void setDisplayValue(float v) override {
ParamQuantity::setDisplayValue(convertTimeInSecondsToCV(v));
}
};

ADSR() {
config(NUM_PARAMS, NUM_INPUTS, NUM_OUTPUTS, NUM_LIGHTS);
configParam(TRIGG_GATE_TOGGLE_PARAM, 0.f, 1.f, 0.f, "Use triggers or gates");
configParam(MANUAL_TRIGGER_PARAM, 0.f, 1.f, 0.f, "Trigger envelope");
configParam(SHAPE_PARAM, 0.f, 1.f, 0.f, "Envelope shape");

configParam<StageTimeParam>(ATTACK_PARAM, 0.f, 1.f, 0.f, "Attack time", "s");
configParam<StageTimeParam>(DECAY_PARAM, 0.f, 1.f, 0.f, "Decay time", "s");
configParam(SUSTAIN_PARAM, 0.f, 1.f, 0.f, "Sustain level", "%", 0.f, 100.f);
configParam<StageTimeParam>(RELEASE_PARAM, 0.f, 1.f, 0.f, "Release time", "s");

cvDivider.setDivision(16);
}

void process(const ProcessArgs& args) override {

bool triggered = gateTrigger.process(rescale(params[MANUAL_TRIGGER_PARAM].getValue() * 10.f + inputs[TRIGGER_INPUT].getVoltage(), 0.1f, 2.f, 0.f, 1.f));

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);
}

bool gateOn = gateTrigger.isHigh() || params[MANUAL_TRIGGER_PARAM].getValue();

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 == ADSREnvelope::STAGE_ATTACK));
outputs[STAGE_DECAY_OUTPUT].setVoltage(10.f * (envelope.stage == ADSREnvelope::STAGE_DECAY));
outputs[STAGE_SUSTAIN_OUTPUT].setVoltage(10.f * (envelope.stage == ADSREnvelope::STAGE_SUSTAIN));
outputs[STAGE_RELEASE_OUTPUT].setVoltage(10.f * (envelope.stage == ADSREnvelope::STAGE_RELEASE));

lights[LED_ATTACK_LIGHT].setBrightness((envelope.stage == ADSREnvelope::STAGE_ATTACK));
lights[LED_DECAY_LIGHT].setBrightness((envelope.stage == ADSREnvelope::STAGE_DECAY));
lights[LED_SUSTAIN_LIGHT].setBrightness((envelope.stage == ADSREnvelope::STAGE_SUSTAIN));
lights[LED_RELEASE_LIGHT].setBrightness((envelope.stage == ADSREnvelope::STAGE_RELEASE));
lights[LED_LIGHT].setBrightness((float) gateOn);
}
};


struct ADSRWidget : ModuleWidget {
ADSRWidget(ADSR* module) {
setModule(module);
setPanel(APP->window->loadSvg(asset::plugin(pluginInstance, "res/ADSR.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<BefacoSwitch>(mm2px(Vec(20.263, 17.128)), module, ADSR::TRIGG_GATE_TOGGLE_PARAM));
addParam(createParamCentered<BefacoPush>(mm2px(Vec(11.581, 32.473)), module, ADSR::MANUAL_TRIGGER_PARAM));
addParam(createParamCentered<BefacoTinyKnob>(mm2px(Vec(29.063, 32.573)), module, ADSR::SHAPE_PARAM));
addParam(createParam<BefacoSlidePot>(mm2px(Vec(2.294, 45.632)), module, ADSR::ATTACK_PARAM));
addParam(createParam<BefacoSlidePot>(mm2px(Vec(12.422, 45.632)), module, ADSR::DECAY_PARAM));
addParam(createParam<BefacoSlidePot>(mm2px(Vec(22.551, 45.632)), module, ADSR::SUSTAIN_PARAM));
addParam(createParam<BefacoSlidePot>(mm2px(Vec(32.68, 45.632)), module, ADSR::RELEASE_PARAM));

addInput(createInputCentered<BefacoInputPort>(mm2px(Vec(6.841, 15.5)), module, ADSR::TRIGGER_INPUT));
addInput(createInputCentered<BefacoInputPort>(mm2px(Vec(5.022, 113.506)), module, ADSR::CV_ATTACK_INPUT));
addInput(createInputCentered<BefacoInputPort>(mm2px(Vec(15.195, 113.506)), module, ADSR::CV_DECAY_INPUT));
addInput(createInputCentered<BefacoInputPort>(mm2px(Vec(25.368, 113.506)), module, ADSR::CV_SUSTAIN_INPUT));
addInput(createInputCentered<BefacoInputPort>(mm2px(Vec(35.541, 113.506)), module, ADSR::CV_RELEASE_INPUT));

addOutput(createOutputCentered<BefacoOutputPort>(mm2px(Vec(33.721, 15.479)), module, ADSR::OUT_OUTPUT));
addOutput(createOutputCentered<BefacoOutputPort>(mm2px(Vec(5.022, 100.858)), module, ADSR::STAGE_ATTACK_OUTPUT));
addOutput(createOutputCentered<BefacoOutputPort>(mm2px(Vec(15.195, 100.858)), module, ADSR::STAGE_DECAY_OUTPUT));
addOutput(createOutputCentered<BefacoOutputPort>(mm2px(Vec(25.368, 100.858)), module, ADSR::STAGE_SUSTAIN_OUTPUT));
addOutput(createOutputCentered<BefacoOutputPort>(mm2px(Vec(35.541, 100.858)), module, ADSR::STAGE_RELEASE_OUTPUT));

addChild(createLightCentered<SmallLight<RedLight>>(mm2px(Vec(20.254, 40.864)), module, ADSR::LED_LIGHT));
addChild(createLightCentered<SmallLight<RedLight>>(mm2px(Vec(5.001, 92.893)), module, ADSR::LED_ATTACK_LIGHT));
addChild(createLightCentered<SmallLight<RedLight>>(mm2px(Vec(15.174, 92.893)), module, ADSR::LED_DECAY_LIGHT));
addChild(createLightCentered<SmallLight<RedLight>>(mm2px(Vec(25.347, 92.893)), module, ADSR::LED_SUSTAIN_LIGHT));
addChild(createLightCentered<SmallLight<RedLight>>(mm2px(Vec(35.52, 92.893)), module, ADSR::LED_RELEASE_LIGHT));
}
};


Model* modelADSR = createModel<ADSR, ADSRWidget>("ADSR");

+ 1
- 0
src/plugin.cpp View File

@@ -19,4 +19,5 @@ void init(rack::Plugin *p) {
p->addModel(modelKickall);
p->addModel(modelSamplingModulator);
p->addModel(modelMorphader);
p->addModel(modelADSR);
}

+ 1
- 0
src/plugin.hpp View File

@@ -19,6 +19,7 @@ extern Model* modelChoppingKinky;
extern Model* modelKickall;
extern Model* modelSamplingModulator;
extern Model* modelMorphader;
extern Model* modelADSR;


struct Knurlie : SvgScrew {


Loading…
Cancel
Save