diff --git a/plugin.json b/plugin.json index 23f28c7..86f5184 100644 --- a/plugin.json +++ b/plugin.json @@ -87,7 +87,8 @@ "name": "ADSR", "description": "Generates an envelope with Attack/Decay/Sustain/Release", "tags": [ - "Envelope Generator" + "Envelope Generator", + "Polyphonic" ] }, { diff --git a/src/ADSR.cpp b/src/ADSR.cpp index 75ef464..226697e 100644 --- a/src/ADSR.cpp +++ b/src/ADSR.cpp @@ -1,6 +1,14 @@ #include "plugin.hpp" +using namespace simd; + + +const float MIN_TIME = 1e-3f; +const float MAX_TIME = 10.f; +const float LAMBDA_BASE = MAX_TIME / MIN_TIME; + + struct ADSR : Module { enum ParamIds { ATTACK_PARAM, @@ -30,77 +38,112 @@ struct ADSR : Module { NUM_LIGHTS }; - bool decaying = false; - float env = 0.f; - dsp::SchmittTrigger trigger; + float_4 attacking[4] = {float_4::zero()}; + float_4 env[4] = {0.f}; + dsp::TSchmittTrigger trigger[4]; + dsp::ClockDivider cvDivider; + float_4 attackLambda[4] = {0.f}; + float_4 decayLambda[4] = {0.f}; + float_4 releaseLambda[4] = {0.f}; + float_4 sustain[4] = {0.f}; + dsp::ClockDivider lightDivider; ADSR() { config(NUM_PARAMS, NUM_INPUTS, NUM_OUTPUTS, NUM_LIGHTS); - configParam(ATTACK_PARAM, 0.f, 1.f, 0.5f, "Attack"); - configParam(DECAY_PARAM, 0.f, 1.f, 0.5f, "Decay"); - configParam(SUSTAIN_PARAM, 0.f, 1.f, 0.5f, "Sustain"); - configParam(RELEASE_PARAM, 0.f, 1.f, 0.5f, "Release"); + configParam(ATTACK_PARAM, 0.f, 1.f, 0.5f, "Attack", " ms", LAMBDA_BASE, MIN_TIME * 1000); + configParam(DECAY_PARAM, 0.f, 1.f, 0.5f, "Decay", " ms", LAMBDA_BASE, MIN_TIME * 1000); + configParam(SUSTAIN_PARAM, 0.f, 1.f, 0.5f, "Sustain", "%", 0, 100); + configParam(RELEASE_PARAM, 0.f, 1.f, 0.5f, "Release", " ms", LAMBDA_BASE, MIN_TIME * 1000); + + cvDivider.setDivision(16); + lightDivider.setDivision(128); } void process(const ProcessArgs &args) override { - float attack = clamp(params[ATTACK_PARAM].getValue() + inputs[ATTACK_INPUT].getVoltage() / 10.f, 0.f, 1.f); - float decay = clamp(params[DECAY_PARAM].getValue() + inputs[DECAY_INPUT].getVoltage() / 10.f, 0.f, 1.f); - float sustain = clamp(params[SUSTAIN_PARAM].getValue() + inputs[SUSTAIN_INPUT].getVoltage() / 10.f, 0.f, 1.f); - float release = clamp(params[RELEASE_PARAM].getValue() + inputs[RELEASE_INPUT].getVoltage() / 10.f, 0.f, 1.f); - - // Gate and trigger - bool gated = inputs[GATE_INPUT].getVoltage() >= 1.f; - if (trigger.process(inputs[TRIG_INPUT].getVoltage())) - decaying = false; - - const float base = 20000.f; - const float maxTime = 10.f; - if (gated) { - if (decaying) { - // Decay - if (decay < 1e-4) { - env = sustain; - } - else { - env += std::pow(base, 1 - decay) / maxTime * (sustain - env) * args.sampleTime; - } - } - else { - // Attack - // Skip ahead if attack is all the way down (infinitely fast) - if (attack < 1e-4) { - env = 1.f; - } - else { - env += std::pow(base, 1 - attack) / maxTime * (1.01f - env) * args.sampleTime; - } - if (env >= 1.f) { - env = 1.f; - decaying = true; - } + // 0.16-0.19 us serial + // 0.23 us serial with all lambdas computed + // 0.15-0.18 us serial with all lambdas computed with SSE + + int channels = inputs[GATE_INPUT].getChannels(); + + // Compute lambdas + if (cvDivider.process()) { + float attackParam = params[ATTACK_PARAM].getValue(); + float decayParam = params[DECAY_PARAM].getValue(); + float sustainParam = params[SUSTAIN_PARAM].getValue(); + float releaseParam = params[RELEASE_PARAM].getValue(); + + for (int c = 0; c < channels; c += 4) { + // CV + float_4 attack = attackParam + inputs[ATTACK_INPUT].getPolyVoltageSimd(c) / 10.f; + float_4 decay = decayParam + inputs[DECAY_INPUT].getPolyVoltageSimd(c) / 10.f; + float_4 sustain = sustainParam + inputs[SUSTAIN_INPUT].getPolyVoltageSimd(c) / 10.f; + float_4 release = releaseParam + inputs[RELEASE_INPUT].getPolyVoltageSimd(c) / 10.f; + + attack = simd::clamp(attack, 0.f, 1.f); + decay = simd::clamp(decay, 0.f, 1.f); + sustain = simd::clamp(sustain, 0.f, 1.f); + release = simd::clamp(release, 0.f, 1.f); + + attackLambda[c / 4] = simd::pow(LAMBDA_BASE, -attack) / MIN_TIME; + decayLambda[c / 4] = simd::pow(LAMBDA_BASE, -decay) / MIN_TIME; + releaseLambda[c / 4] = simd::pow(LAMBDA_BASE, -release) / MIN_TIME; + this->sustain[c / 4] = sustain; } } - else { - // Release - if (release < 1e-4) { - env = 0.f; - } - else { - env += std::pow(base, 1 - release) / maxTime * (0.f - env) * args.sampleTime; - } - decaying = false; - } - bool sustaining = isNear(env, sustain, 1e-3); - bool resting = isNear(env, 0.f, 1e-3); + float_4 gate[4]; + + for (int c = 0; c < channels; c += 4) { + // Gate + gate[c / 4] = inputs[GATE_INPUT].getVoltageSimd(c) >= 1.f; - outputs[ENVELOPE_OUTPUT].setVoltage(10.f * env); + // Retrigger + float_4 triggered = trigger[c / 4].process(inputs[TRIG_INPUT].getPolyVoltageSimd(c)); + attacking[c / 4] = simd::ifelse(triggered, float_4::mask(), attacking[c / 4]); + + // Get target and lambda for exponential decay + const float attackTarget = 1.2f; + float_4 target = simd::ifelse(gate[c / 4], simd::ifelse(attacking[c / 4], attackTarget, sustain[c / 4]), 0.f); + float_4 lambda = simd::ifelse(gate[c / 4], simd::ifelse(attacking[c / 4], attackLambda[c / 4], decayLambda[c / 4]), releaseLambda[c / 4]); + + // Adjust env + env[c / 4] += (target - env[c / 4]) * lambda * args.sampleTime; + + // Turn off attacking state if envelope is HIGH + attacking[c / 4] = simd::ifelse(env[c / 4] >= 1.f, float_4::zero(), attacking[c / 4]); + + // Turn on attacking state if gate is LOW + attacking[c / 4] = simd::ifelse(gate[c / 4], attacking[c / 4], float_4::mask()); + + // Set output + outputs[ENVELOPE_OUTPUT].setVoltageSimd(10.f * env[c / 4], c); + } + + outputs[ENVELOPE_OUTPUT].setChannels(channels); // Lights - lights[ATTACK_LIGHT].value = (gated && !decaying) ? 1.f : 0.f; - lights[DECAY_LIGHT].value = (gated && decaying && !sustaining) ? 1.f : 0.f; - lights[SUSTAIN_LIGHT].value = (gated && decaying && sustaining) ? 1.f : 0.f; - lights[RELEASE_LIGHT].value = (!gated && !resting) ? 1.f : 0.f; + if (lightDivider.process()) { + lights[ATTACK_LIGHT].setBrightness(0); + lights[DECAY_LIGHT].setBrightness(0); + lights[SUSTAIN_LIGHT].setBrightness(0); + lights[RELEASE_LIGHT].setBrightness(0); + + for (int c = 0; c < channels; c += 4) { + const float epsilon = 0.01f; + float_4 sustaining = (sustain[c / 4] <= env[c / 4]) & (env[c / 4] < sustain[c / 4] + epsilon); + float_4 resting = (env[c / 4] < epsilon); + + if (simd::movemask(gate[c / 4] & attacking[c / 4])) + lights[ATTACK_LIGHT].setBrightness(1); + if (simd::movemask(gate[c / 4] & ~attacking[c / 4] & ~sustaining)) + lights[DECAY_LIGHT].setBrightness(1); + if (simd::movemask(gate[c / 4] & ~attacking[c / 4] & sustaining)) + lights[SUSTAIN_LIGHT].setBrightness(1); + if (simd::movemask(~gate[c / 4] & ~resting)) + lights[RELEASE_LIGHT].setBrightness(1); + } + } } }; @@ -111,9 +154,9 @@ struct ADSRWidget : ModuleWidget { setPanel(APP->window->loadSvg(asset::plugin(pluginInstance, "res/ADSR.svg"))); addChild(createWidget(Vec(15, 0))); - addChild(createWidget(Vec(box.size.x-30, 0))); + addChild(createWidget(Vec(box.size.x - 30, 0))); addChild(createWidget(Vec(15, 365))); - addChild(createWidget(Vec(box.size.x-30, 365))); + addChild(createWidget(Vec(box.size.x - 30, 365))); addParam(createParam(Vec(62, 57), module, ADSR::ATTACK_PARAM)); addParam(createParam(Vec(62, 124), module, ADSR::DECAY_PARAM)); diff --git a/src/Delay.cpp b/src/Delay.cpp index 139b3cb..8eeba96 100644 --- a/src/Delay.cpp +++ b/src/Delay.cpp @@ -36,7 +36,7 @@ struct Delay : Module { config(NUM_PARAMS, NUM_INPUTS, NUM_OUTPUTS); configParam(TIME_PARAM, 0.f, 1.f, 0.5f, "Time", " s", 10.f / 1e-3, 1e-3); configParam(FEEDBACK_PARAM, 0.f, 1.f, 0.5f, "Feedback", "%", 0, 100); - configParam(COLOR_PARAM, 0.f, 1.f, 0.5f, "Color"); + configParam(COLOR_PARAM, 0.f, 1.f, 0.5f, "Color", "%", 0, 100); configParam(MIX_PARAM, 0.f, 1.f, 0.5f, "Mix", "%", 0, 100); src = src_new(SRC_SINC_FASTEST, 1, NULL); diff --git a/src/Scope.cpp b/src/Scope.cpp index f43d189..a9e90ef 100644 --- a/src/Scope.cpp +++ b/src/Scope.cpp @@ -39,7 +39,7 @@ struct Scope : Module { int channelsX = 0; int channelsY = 0; int bufferIndex = 0; - float frameIndex = 0; + int frameIndex = 0; dsp::BooleanTrigger sumTrigger; dsp::BooleanTrigger extTrigger; @@ -120,19 +120,21 @@ struct Scope : Module { return; } - // Reset the Schmitt trigger so we don't trigger immediately if the input is high - if (frameIndex == 0) { - resetTrigger.reset(); - } frameIndex++; - // Must go below 0.1V to trigger - float gate = external ? inputs[TRIG_INPUT].getVoltage() : inputs[X_INPUT].getVoltage(); - // Reset if triggered - float holdTime = 0.1f; float trigValue = params[TRIG_PARAM].getValue(); - if (resetTrigger.process(rescale(gate, trigValue - 0.1f, trigValue, 0.f, 1.f)) || (frameIndex >= args.sampleRate * holdTime)) { + float gate = external ? inputs[TRIG_INPUT].getVoltage() : inputs[X_INPUT].getVoltage(); + + if (resetTrigger.process(rescale(gate, trigValue, trigValue + 0.001f, 0.f, 1.f))) { + bufferIndex = 0; + frameIndex = 0; + return; + } + + // Reset if we've been waiting for `holdTime` + const float holdTime = 0.5f; + if (frameIndex * args.sampleTime >= holdTime) { bufferIndex = 0; frameIndex = 0; return; @@ -165,22 +167,18 @@ struct ScopeDisplay : TransparentWidget { std::shared_ptr font; struct Stats { - // float vrms = 0.f; float vpp = 0.f; float vmin = 0.f; float vmax = 0.f; void calculate(float *buffer, int channels) { - // vrms = 0.f; vmax = -INFINITY; vmin = INFINITY; for (int i = 0; i < BUFFER_SIZE * channels; i++) { float v = buffer[i]; - // vrms += v*v; vmax = std::fmax(vmax, v); vmin = std::fmin(vmin, v); } - // vrms = std::sqrt(vrms / BUFFER_SIZE); vpp = vmax - vmin; } };