diff --git a/plugin.json b/plugin.json index ce6d69d..39f7e68 100644 --- a/plugin.json +++ b/plugin.json @@ -1,93 +1,104 @@ -{ - "slug": "Befaco", - "version": "1.0.1", - "license": "GPL-3.0-or-later", - "name": "Befaco", - "author": "VCV", - "authorEmail": "contact@vcvrack.com", - "pluginUrl": "https://vcvrack.com/Befaco.html", - "authorUrl": "https://vcvrack.com/", - "sourceUrl": "https://github.com/VCVRack/Befaco", - "modules": [ - { - "slug": "EvenVCO", - "name": "EvenVCO", - "description": "Oscillator including even-harmonic waveform", - "modularGridUrl": "https://www.modulargrid.net/e/befaco-even-vco-", - "tags": [ - "VCO", - "Hardware clone" - ] - }, - { - "slug": "Rampage", - "name": "Rampage", - "description": "Dual ramp generator", - "manualUrl": "https://befaco.org/docs/Rampage/Rampage_User_Manual.pdf", - "modularGridUrl": "https://www.modulargrid.net/e/befaco-rampage", - "tags": [ - "Function Generator", - "Logic", - "Slew Limiter", - "Envelope Follower", - "Dual", - "Hardware clone" - ] - }, - { - "slug": "ABC", - "name": "A*B+C", - "description": "Dual four-quadrant multiplier with VC offset", - "manualUrl": "https://befaco.org/docs/AB%2BC/AB%2BC_User_Manual.pdf", - "modularGridUrl": "https://www.modulargrid.net/e/befaco-a-b-c", - "tags": [ - "Ring Modulator", - "Attenuator", - "Dual", - "Hardware clone" - ] - }, - { - "slug": "SpringReverb", - "name": "Spring Reverb", - "description": "Spring reverb tank driver", - "modularGridUrl": "https://www.modulargrid.net/e/befaco-spring-reverb-", - "tags": [ - "Reverb", - "Hardware clone" - ] - }, - { - "slug": "Mixer", - "name": "Mixer", - "description": "Four-channel mixer for audio or CV", - "modularGridUrl": "https://www.modulargrid.net/e/befaco-mixer-", - "tags": [ - "Mixer", - "Hardware clone" - ] - }, - { - "slug": "SlewLimiter", - "name": "Slew Limiter", - "description": "Voltage controlled slew limiter, AKA lag processor", - "modularGridUrl": "https://www.modulargrid.net/e/befaco-vc-slew-limiter-", - "tags": [ - "Slew Limiter", - "Envelope Follower", - "Hardware clone" - ] - }, - { - "slug": "DualAtenuverter", - "name": "Dual Atenuverter", - "description": "Attenuates, inverts, and applies offset to a signal", - "modularGridUrl": "https://www.modulargrid.net/e/befaco-dual-atenuverter-", - "tags": [ - "Attenuator", - "Dual", - "Hardware clone" - ] - } - ] +{ + "slug": "Befaco", + "version": "1.0.2", + "license": "GPL-3.0-or-later", + "name": "Befaco", + "author": "VCV", + "authorEmail": "contact@vcvrack.com", + "pluginUrl": "https://vcvrack.com/Befaco.html", + "authorUrl": "https://vcvrack.com/", + "sourceUrl": "https://github.com/VCVRack/Befaco", + "modules": [ + { + "slug": "EvenVCO", + "name": "EvenVCO", + "description": "Oscillator including even-harmonic waveform", + "modularGridUrl": "https://www.modulargrid.net/e/befaco-even-vco-", + "tags": [ + "VCO", + "Hardware clone" + ] + }, + { + "slug": "Rampage", + "name": "Rampage", + "description": "Dual ramp generator", + "manualUrl": "https://befaco.org/docs/Rampage/Rampage_User_Manual.pdf", + "modularGridUrl": "https://www.modulargrid.net/e/befaco-rampage", + "tags": [ + "Function Generator", + "Logic", + "Slew Limiter", + "Envelope Follower", + "Dual", + "Hardware clone" + ] + }, + { + "slug": "ABC", + "name": "A*B+C", + "description": "Dual four-quadrant multiplier with VC offset", + "manualUrl": "https://befaco.org/docs/AB%2BC/AB%2BC_User_Manual.pdf", + "modularGridUrl": "https://www.modulargrid.net/e/befaco-a-b-c", + "tags": [ + "Ring Modulator", + "Attenuator", + "Dual", + "Hardware clone" + ] + }, + { + "slug": "SpringReverb", + "name": "Spring Reverb", + "description": "Spring reverb tank driver", + "modularGridUrl": "https://www.modulargrid.net/e/befaco-spring-reverb-", + "tags": [ + "Reverb", + "Hardware clone" + ] + }, + { + "slug": "Mixer", + "name": "Mixer", + "description": "Four-channel mixer for audio or CV", + "modularGridUrl": "https://www.modulargrid.net/e/befaco-mixer-", + "tags": [ + "Mixer", + "Hardware clone" + ] + }, + { + "slug": "SlewLimiter", + "name": "Slew Limiter", + "description": "Voltage controlled slew limiter, AKA lag processor", + "modularGridUrl": "https://www.modulargrid.net/e/befaco-vc-slew-limiter-", + "tags": [ + "Slew Limiter", + "Envelope Follower", + "Hardware clone" + ] + }, + { + "slug": "DualAtenuverter", + "name": "Dual Atenuverter", + "description": "Attenuates, inverts, and applies offset to a signal", + "modularGridUrl": "https://www.modulargrid.net/e/befaco-dual-atenuverter-", + "tags": [ + "Attenuator", + "Dual", + "Hardware clone" + ] + }, + { + "slug": "Percall", + "name": "Percall", + "description": "Percussive Envelope Generator", + "modularGridUrl": "https://www.modulargrid.net/e/befaco-percall", + "tags": [ + "Envelope generator", + "Mixer", + "Hardware clone" + ] + } + ] } \ No newline at end of file diff --git a/res/Percall.svg b/res/Percall.svg new file mode 100644 index 0000000..a144dbf --- /dev/null +++ b/res/Percall.svg @@ -0,0 +1,1251 @@ + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/Percall.cpp b/src/Percall.cpp new file mode 100644 index 0000000..93d195d --- /dev/null +++ b/src/Percall.cpp @@ -0,0 +1,213 @@ +#include "plugin.hpp" + + +static float expDelta(float delta, float tau) { + float lin = sgn(delta) * 10.f / tau; + float exp = M_E * delta / tau; + return crossfade(lin, exp, 0.90f); +} + + +struct Percall : Module { + enum ParamIds { + ENUMS(VOL_PARAMS, 4), + ENUMS(DECAY_PARAMS, 4), + ENUMS(CHOKE_PARAMS, 2), + NUM_PARAMS + }; + enum InputIds { + ENUMS(CH_INPUTS, 4), + STRENGTH_INPUT, + ENUMS(TRIG_INPUTS, 4), + ENUMS(CV_INPUTS, 4), + NUM_INPUTS + }; + enum OutputIds { + ENUMS(CH_OUTPUTS, 4), + ENUMS(ENV_OUTPUTS, 4), + NUM_OUTPUTS + }; + enum LightIds { + ENUMS(LEDS, 4), + NUM_LIGHTS + }; + + enum Stage { + STAGE_OFF, + STAGE_ATTACK, + STAGE_DECAY + }; + + int stage[4] = {}; + float env[4] = {}; + dsp::SchmittTrigger trigger[4]; + const int LAST_CHANNEL_ID = 3; + + Percall() { + config(NUM_PARAMS, NUM_INPUTS, NUM_OUTPUTS, NUM_LIGHTS); + for (int i = 0; i < 4; i++) { + configParam(VOL_PARAMS + i, 0.f, 1.f, 1.f, "Ch " + std::to_string(i) + " level", "%", 0, 100); + configParam(DECAY_PARAMS + i, 0.f, 1.f, 0.f, "Ch " + std::to_string(i) + " decay time"); + } + for (int i = 0; i < 2; i++) { + std::string description = "Choke " + std::to_string(2 * i) + " to " + std::to_string(2 * i + 1); + configParam(CHOKE_PARAMS + i, 0.f, 1.f, 0.f, description); + } + } + + void process(const ProcessArgs& args) override { + float mix[16] = {}; + int maxChannels = 1; + float strength = 1.0f; + if (inputs[STRENGTH_INPUT].isConnected()) { + strength = clamp(inputs[STRENGTH_INPUT].getVoltage() / 10.0f, 0.0f, 1.0f); + } + + // Channels + for (int i = 0; i < 4; i++) { + + if (trigger[i].process(rescale(inputs[TRIG_INPUTS + i].getVoltage(), 0.1f, 2.f, 0.f, 1.f))) { + stage[i] = STAGE_ATTACK; + } + // if choke is enabled, and current channel is odd and left channel is in attack + if ((i % 2) && params[CHOKE_PARAMS + i / 2].getValue() && stage[i - 1] == STAGE_ATTACK) { + // TODO: is there a more graceful way to choke, e.g. rapid envelope? + // TODO: this will just silence it instantly, maybe switch to STAGE_DECAY and modify fall time + stage[i] = STAGE_OFF; + } + + float target_voltage = (stage[i] == STAGE_ATTACK) ? 10.0f : 0.0f; + float delta = target_voltage - env[i]; + + if (stage[i] == STAGE_OFF) { + env[i] = 0.0f; + } + else if (stage[i] == STAGE_ATTACK) { + env[i] += expDelta(delta, 2e-3) * args.sampleTime; + } + else if (stage[i] == STAGE_DECAY) { + float fallCv = inputs[CV_INPUTS + i].getVoltage() / 10.0 + params[DECAY_PARAMS + i].getValue(); + fallCv = clamp(fallCv, 0.0f, 1.0f); + float fall = 0.01 * std::pow(2.0, fallCv * 10.0); + + env[i] += expDelta(delta, fall) * args.sampleTime; + } + if (env[i] > 10.0f) { + stage[i] = STAGE_DECAY; + env[i] = 10.0f; + } + else if (env[i] < 0.0f) { + stage[i] = STAGE_OFF; + env[i] = 0.0f; + } + + lights[LEDS + i].setBrightness(stage[i] != STAGE_OFF); + + int channels = 1; + float in[16] = {}; + bool input_is_connected = inputs[CH_INPUTS + i].isConnected(); + bool input_is_normed = !input_is_connected && (i % 2) && inputs[CH_INPUTS + i - 1].isConnected(); + if (input_is_connected || input_is_normed) { + int channel_to_read_from = input_is_normed ? CH_INPUTS + i - 1 : CH_INPUTS + i; + channels = inputs[channel_to_read_from].getChannels(); + maxChannels = std::max(maxChannels, channels); + + // Get input + inputs[channel_to_read_from].readVoltages(in); + + // Apply fader gain + float gain = std::pow(params[VOL_PARAMS + i].getValue(), 2.f); + for (int c = 0; c < channels; c++) { + in[c] *= gain * env[i] / 10.0f * strength; + } + } + + if (i != LAST_CHANNEL_ID) { + // if connected, output via the jack (and don't add to mix) + if (outputs[CH_OUTPUTS + i].isConnected()) { + outputs[CH_OUTPUTS + i].setChannels(channels); + outputs[CH_OUTPUTS + i].writeVoltages(in); + } + else { + // else add to mix + for (int c = 0; c < channels; c++) { + mix[c] += in[c]; + } + } + } + else { + // last channel must always go into mix + for (int c = 0; c < channels; c++) { + mix[c] += in[c]; + } + if (outputs[CH_OUTPUTS + i].isConnected()) { + outputs[CH_OUTPUTS + i].setChannels(channels); + outputs[CH_OUTPUTS + i].writeVoltages(mix); + } + } + + // set env output + if (outputs[ENV_OUTPUTS + i].isConnected()) { + outputs[ENV_OUTPUTS + i].setVoltage(strength * env[i]); + } + } + } +}; + + +struct PercallWidget : ModuleWidget { + PercallWidget(Percall* module) { + setModule(module); + setPanel(APP->window->loadSvg(asset::plugin(pluginInstance, "res/Percall.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(8.003, 41.196)), module, Percall::VOL_PARAMS + 0)); + addParam(createParamCentered(mm2px(Vec(22.829, 41.196)), module, Percall::VOL_PARAMS + 1)); + addParam(createParamCentered(mm2px(Vec(37.655, 41.196)), module, Percall::VOL_PARAMS + 2)); + addParam(createParamCentered(mm2px(Vec(52.481, 41.196)), module, Percall::VOL_PARAMS + 3)); + addParam(createParam(mm2px(Vec(5.385, 52.476)), module, Percall::DECAY_PARAMS + 0)); + addParam(createParam(mm2px(Vec(20.728, 52.476)), module, Percall::DECAY_PARAMS + 1)); + addParam(createParam(mm2px(Vec(35.543, 52.476)), module, Percall::DECAY_PARAMS + 2)); + addParam(createParam(mm2px(Vec(50.357, 52.476)), module, Percall::DECAY_PARAMS + 3)); + addParam(createParam(mm2px(Vec(13.365, 58.672)), module, Percall::CHOKE_PARAMS + 0)); + addParam(createParam(mm2px(Vec(42.993, 58.672)), module, Percall::CHOKE_PARAMS + 1)); + + addInput(createInputCentered(mm2px(Vec(7.173, 12.894)), module, Percall::CH_INPUTS + 0)); + addInput(createInputCentered(mm2px(Vec(20.335, 12.894)), module, Percall::CH_INPUTS + 1)); + addInput(createInputCentered(mm2px(Vec(40.347, 12.894)), module, Percall::CH_INPUTS + 2)); + addInput(createInputCentered(mm2px(Vec(53.492, 12.894)), module, Percall::CH_INPUTS + 3)); + + + addInput(createInputCentered(mm2px(Vec(30.341, 18.236)), module, Percall::STRENGTH_INPUT)); + addInput(createInputCentered(mm2px(Vec(7.173, 24.834)), module, Percall::TRIG_INPUTS + 0)); + addInput(createInputCentered(mm2px(Vec(18.547, 23.904)), module, Percall::TRIG_INPUTS + 1)); + addInput(createInputCentered(mm2px(Vec(42.218, 23.904)), module, Percall::TRIG_INPUTS + 2)); + addInput(createInputCentered(mm2px(Vec(53.453, 24.834)), module, Percall::TRIG_INPUTS + 3)); + addInput(createInputCentered(mm2px(Vec(5.093, 101.799)), module, Percall::CV_INPUTS + 0)); + addInput(createInputCentered(mm2px(Vec(15.22, 101.799)), module, Percall::CV_INPUTS + 1)); + addInput(createInputCentered(mm2px(Vec(25.347, 101.799)), module, Percall::CV_INPUTS + 2)); + addInput(createInputCentered(mm2px(Vec(35.474, 101.799)), module, Percall::CV_INPUTS + 3)); + + addOutput(createOutputCentered(mm2px(Vec(45.541, 101.699)), module, Percall::CH_OUTPUTS + 0)); + addOutput(createOutputCentered(mm2px(Vec(55.624, 101.699)), module, Percall::CH_OUTPUTS + 1)); + addOutput(createOutputCentered(mm2px(Vec(45.541, 113.696)), module, Percall::CH_OUTPUTS + 2)); + addOutput(createOutputCentered(mm2px(Vec(55.624, 113.696)), module, Percall::CH_OUTPUTS + 3)); + + addOutput(createOutputCentered(mm2px(Vec(5.093, 113.74)), module, Percall::ENV_OUTPUTS + 0)); + addOutput(createOutputCentered(mm2px(Vec(15.22, 113.74)), module, Percall::ENV_OUTPUTS + 1)); + addOutput(createOutputCentered(mm2px(Vec(25.347, 113.74)), module, Percall::ENV_OUTPUTS + 2)); + addOutput(createOutputCentered(mm2px(Vec(35.474, 113.74)), module, Percall::ENV_OUTPUTS + 3)); + + addChild(createLightCentered>(mm2px(Vec(8.107, 49.221)), module, Percall::LEDS + 0)); + addChild(createLightCentered>(mm2px(Vec(22.934, 49.221)), module, Percall::LEDS + 1)); + addChild(createLightCentered>(mm2px(Vec(37.762, 49.221)), module, Percall::LEDS + 2)); + addChild(createLightCentered>(mm2px(Vec(52.589, 49.221)), module, Percall::LEDS + 3)); + } +}; + + +Model* modelPercall = createModel("Percall"); \ No newline at end of file diff --git a/src/plugin.cpp b/src/plugin.cpp index e9d2840..ef5e791 100644 --- a/src/plugin.cpp +++ b/src/plugin.cpp @@ -13,4 +13,5 @@ void init(rack::Plugin *p) { p->addModel(modelMixer); p->addModel(modelSlewLimiter); p->addModel(modelDualAtenuverter); + p->addModel(modelPercall); } diff --git a/src/plugin.hpp b/src/plugin.hpp index 05674b8..cb15bf0 100644 --- a/src/plugin.hpp +++ b/src/plugin.hpp @@ -13,6 +13,7 @@ extern Model *modelSpringReverb; extern Model *modelMixer; extern Model *modelSlewLimiter; extern Model *modelDualAtenuverter; +extern Model *modelPercall; struct Knurlie : SVGScrew {