diff --git a/CHANGELOG.md b/CHANGELOG.md index b6e9650..031c8c6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,11 @@ # Change Log +## v2.5.0 + * Burst + * Initial release + * PonyVCO + * Now polyphonic + ## v2.4.0 * MotionMTR * Initial release diff --git a/plugin.json b/plugin.json index 8edb3d1..621798e 100644 --- a/plugin.json +++ b/plugin.json @@ -1,6 +1,6 @@ { "slug": "Befaco", - "version": "2.4.0", + "version": "2.5.0", "license": "GPL-3.0-or-later", "name": "Befaco", "brand": "Befaco", @@ -267,6 +267,7 @@ "Hardware clone", "Low-frequency oscillator", "Oscillator", + "Polyphonic", "Waveshaper" ] }, @@ -282,6 +283,18 @@ "Mixer", "Visual" ] + }, + { + "slug": "Burst", + "name": "Burst", + "description": "Trigger processor and generator, designed to add an organic chain of events", + "manualUrl": "https://www.befaco.org/burst-2/", + "modularGridUrl": "https://www.modulargrid.net/e/befaco-burst-", + "tags": [ + "Clock generator", + "Clock modulator", + "Hardware clone" + ] } ] } \ No newline at end of file diff --git a/res/components/Davies1900hWhiteEndless.svg b/res/components/Davies1900hWhiteEndless.svg new file mode 100644 index 0000000..4eacfa0 --- /dev/null +++ b/res/components/Davies1900hWhiteEndless.svg @@ -0,0 +1,76 @@ + + + + + + + + + + + + + + diff --git a/res/components/Davies1900hWhiteEndless_bg.svg b/res/components/Davies1900hWhiteEndless_bg.svg new file mode 100644 index 0000000..a8f37b6 --- /dev/null +++ b/res/components/Davies1900hWhiteEndless_bg.svg @@ -0,0 +1,24 @@ + + + + + + + + + + + + + + + + + + + + + diff --git a/res/panels/Burst.svg b/res/panels/Burst.svg new file mode 100644 index 0000000..cf45e7b --- /dev/null +++ b/res/panels/Burst.svg @@ -0,0 +1,1131 @@ + + + + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/Burst.cpp b/src/Burst.cpp new file mode 100644 index 0000000..f8108ab --- /dev/null +++ b/src/Burst.cpp @@ -0,0 +1,349 @@ +#include "plugin.hpp" + +#define MAX_REPETITIONS 32 /// max number of repetitions +#define TRIGGER_TIME 0.001 + +// a tempo/clock calculator that responds to pings - this sets the base tempo, multiplication/division of +// this tempo occurs in the BurstEngine +struct PingableClock { + + dsp::Timer timer; // time the gap between pings + dsp::PulseGenerator clockTimer; // counts down from tempo length to zero + dsp::BooleanTrigger clockExpiry; // checks for when the clock timer runs out + + float pingDuration = 0.5f; // used for calculating and updating tempo (default 2Hz / 120 bpm) + float tempo = 0.5f; // actual current tempo of clock + + PingableClock() { + clockTimer.trigger(tempo); + } + + void process(bool pingRecieved, float sampleTime) { + timer.process(sampleTime); + + bool clockRestarted = false; + + if (pingRecieved) { + + bool tempoShouldBeUpdated = true; + float duration = timer.getTime(); + + // if the ping was unusually different to last time + bool outlier = duration > (pingDuration * 2) || duration < (pingDuration / 2); + // if there is a previous estimate of tempo, but it's an outlier + if ((pingDuration && outlier)) { + // don't calculate tempo from this; prime so future pings will update + tempoShouldBeUpdated = false; + pingDuration = 0; + } + else { + pingDuration = duration; + } + timer.reset(); + + if (tempoShouldBeUpdated) { + // if the tempo should be updated, do so + tempo = pingDuration; + clockRestarted = true; + } + } + + // we restart the clock if a) a new valid ping arrived OR b) the current clock expired + clockRestarted = clockExpiry.process(!clockTimer.process(sampleTime)) || clockRestarted; + if (clockRestarted) { + clockTimer.reset(); + clockTimer.trigger(tempo); + } + } + + bool isTempoOutHigh() { + // give a 1ms pulse as tempo out + return clockTimer.remaining > tempo - TRIGGER_TIME; + } +}; + +// engine that generates a burst when triggered +struct BurstEngine { + + dsp::PulseGenerator eocOutput; // for generating EOC trigger + dsp::PulseGenerator burstOutput; // for generating triggers for each occurance of the burst + dsp::Timer burstTimer; // for timing how far through the current burst we are + + float timings[MAX_REPETITIONS + 1] = {}; // store timings (calculated once on burst trigger) + + int triggersOccurred = 0; // how many triggers have been + int triggersRequested = 0; // how many bursts have been requested (fixed over course of burst) + bool active = true; // is there a burst active + bool wasInhibited = false; // was this burst inhibited (i.e. just the first trigger sent) + + std::tuple process(float sampleTime) { + + if (active) { + burstTimer.process(sampleTime); + } + + bool eocTriggered = false; + if (burstTimer.time > timings[triggersOccurred]) { + if (triggersOccurred < triggersRequested) { + burstOutput.reset(); + burstOutput.trigger(TRIGGER_TIME); + } + else if (triggersOccurred == triggersRequested) { + eocOutput.reset(); + eocOutput.trigger(TRIGGER_TIME); + active = false; + eocTriggered = true; + } + triggersOccurred++; + } + + const float burstOut = burstOutput.process(sampleTime); + // NOTE: we don't get EOC if the burst was inhibited + const float eocOut = eocOutput.process(sampleTime) * !wasInhibited; + return std::make_tuple(burstOut, eocOut, eocTriggered); + } + + void trigger(int numBursts, int multDiv, float baseTimeWindow, float distribution, bool inhibitBurst, bool includeOriginalTrigger) { + + active = true; + wasInhibited = inhibitBurst; + + // the window in which the burst fits is a multiple (or division) of the base tempo + int divisions = multDiv + (multDiv > 0 ? 1 : multDiv < 0 ? -1 : 0); // skip 2/-2 + float actualTimeWindow = baseTimeWindow; + if (divisions > 0) { + actualTimeWindow = baseTimeWindow * divisions; + } + else if (divisions < 0) { + actualTimeWindow = baseTimeWindow / (-divisions); + } + + // calculate the times at which triggers should fire, will be skewed by distribution + const float power = 1 + std::abs(distribution) * 2; + for (int i = 0; i <= numBursts; ++i) { + if (distribution >= 0) { + timings[i] = actualTimeWindow * std::pow((float)i / numBursts, power); + } + else { + timings[i] = actualTimeWindow * std::pow((float)i / numBursts, 1 / power); + } + } + + triggersOccurred = includeOriginalTrigger ? 0 : 1; + triggersRequested = inhibitBurst ? 1 : numBursts; + burstTimer.reset(); + } +}; + +struct Burst : Module { + enum ParamIds { + CYCLE_PARAM, + QUANTITY_PARAM, + TRIGGER_PARAM, + QUANTITY_CV_PARAM, + DISTRIBUTION_PARAM, + TIME_PARAM, + PROBABILITY_PARAM, + NUM_PARAMS + }; + enum InputIds { + QUANTITY_INPUT, + DISTRIBUTION_INPUT, + PING_INPUT, + TIME_INPUT, + PROBABILITY_INPUT, + TRIGGER_INPUT, + NUM_INPUTS + }; + enum OutputIds { + TEMPO_OUTPUT, + EOC_OUTPUT, + OUT_OUTPUT, + NUM_OUTPUTS + }; + enum LightIds { + ENUMS(QUANTITY_LIGHTS, 16), + TEMPO_LIGHT, + EOC_LIGHT, + OUT_LIGHT, + NUM_LIGHTS + }; + + + dsp::SchmittTrigger pingTrigger; // for detecting Ping in + dsp::SchmittTrigger triggTrigger; // for detecting Trigg in + dsp::BooleanTrigger buttonTrigger; // for detecting when the trigger button is pressed + dsp::ClockDivider ledUpdate; // for only updating LEDs every N samples + const int ledUpdateRate = 16; // LEDs updated every N = 16 samples + + PingableClock pingableClock; + BurstEngine burstEngine; + bool includeOriginalTrigger = true; + + Burst() { + config(NUM_PARAMS, NUM_INPUTS, NUM_OUTPUTS, NUM_LIGHTS); + configSwitch(Burst::CYCLE_PARAM, 0.0, 1.0, 0.0, "Mode", {"One-shot", "Cycle"}); + auto quantityParam = configParam(Burst::QUANTITY_PARAM, 1, MAX_REPETITIONS, 0, "Number of bursts"); + quantityParam->snapEnabled = true; + configButton(Burst::TRIGGER_PARAM, "Manual Trigger"); + configParam(Burst::QUANTITY_CV_PARAM, 0.0, 1.0, 1.0, "Quantity CV"); + configParam(Burst::DISTRIBUTION_PARAM, -1.0, 1.0, 0.0, "Distribution"); + auto timeParam = configParam(Burst::TIME_PARAM, -4.0, 4.0, 0.0, "Time Division/Multiplication"); + timeParam->snapEnabled = true; + configParam(Burst::PROBABILITY_PARAM, 0.0, 1.0, 0.0, "Probability", "%", 0.f, -100, 100.); + + configInput(QUANTITY_INPUT, "Quantity CV"); + configInput(DISTRIBUTION_INPUT, "Distribution"); + configInput(PING_INPUT, "Ping"); + configInput(TIME_INPUT, "Time Division/Multiplication"); + configInput(PROBABILITY_INPUT, "Probability"); + configInput(TRIGGER_INPUT, "Trigger"); + + ledUpdate.setDivision(ledUpdateRate); + } + + void process(const ProcessArgs& args) override { + + const bool pingReceived = pingTrigger.process(inputs[PING_INPUT].getVoltage()); + pingableClock.process(pingReceived, args.sampleTime); + + if (ledUpdate.process()) { + updateLEDRing(args); + } + + const float quantityCV = params[QUANTITY_CV_PARAM].getValue() * clamp(inputs[QUANTITY_INPUT].getVoltage(), -5.0, +10.f) / 5.f; + const int quantity = clamp((int)(params[QUANTITY_PARAM].getValue() + std::round(16 * quantityCV)), 1, MAX_REPETITIONS); + + const bool loop = params[CYCLE_PARAM].getValue(); + + const float divMultCV = 4.0 * inputs[TIME_INPUT].getVoltage() / 10.f; + const int divMult = -clamp((int)(divMultCV + params[TIME_PARAM].getValue()), -4, +4); + + const float distributionCV = inputs[DISTRIBUTION_INPUT].getVoltage() / 10.f; + const float distribution = clamp(distributionCV + params[DISTRIBUTION_PARAM].getValue(), -1.f, +1.f); + + const bool triggerInputTriggered = triggTrigger.process(inputs[TRIGGER_INPUT].getVoltage()); + const bool triggerButtonTriggered = buttonTrigger.process(params[TRIGGER_PARAM].getValue()); + const bool startBurst = triggerInputTriggered || triggerButtonTriggered; + + if (startBurst) { + const float prob = clamp(params[PROBABILITY_PARAM].getValue() + inputs[PROBABILITY_INPUT].getVoltage() / 10.f, 0.f, 1.f); + const bool inhibitBurst = rack::random::uniform() < prob; + + // remember to do at current tempo + burstEngine.trigger(quantity, divMult, pingableClock.tempo, distribution, inhibitBurst, includeOriginalTrigger); + } + + float burstOut, eocOut; + bool eoc; + std::tie(burstOut, eocOut, eoc) = burstEngine.process(args.sampleTime); + + // if the burst has finished, we can also re-trigger + if (eoc && loop) { + const float prob = clamp(params[PROBABILITY_PARAM].getValue() + inputs[PROBABILITY_INPUT].getVoltage() / 10.f, 0.f, 1.f); + const bool inhibitBurst = rack::random::uniform() < prob; + + // remember to do at current tempo + burstEngine.trigger(quantity, divMult, pingableClock.tempo, distribution, inhibitBurst, includeOriginalTrigger); + } + + const bool tempoOutHigh = pingableClock.isTempoOutHigh(); + outputs[TEMPO_OUTPUT].setVoltage(10.f * tempoOutHigh); + lights[TEMPO_LIGHT].setBrightnessSmooth(tempoOutHigh, args.sampleTime); + + outputs[OUT_OUTPUT].setVoltage(10.f * burstOut); + lights[OUT_LIGHT].setBrightnessSmooth(burstOut, args.sampleTime); + + outputs[EOC_OUTPUT].setVoltage(10.f * eocOut); + lights[EOC_LIGHT].setBrightnessSmooth(eocOut, args.sampleTime); + } + + void updateLEDRing(const ProcessArgs& args) { + int activeLed; + if (burstEngine.active) { + activeLed = (burstEngine.triggersOccurred - 1) % 16; + } + else { + activeLed = (((int) params[QUANTITY_PARAM].getValue() - 1) % 16); + } + for (int i = 0; i < 16; ++i) { + lights[QUANTITY_LIGHTS + i].setBrightnessSmooth(i == activeLed, args.sampleTime * ledUpdateRate); + } + } + + json_t* dataToJson() override { + json_t* rootJ = json_object(); + json_object_set_new(rootJ, "includeOriginalTrigger", json_boolean(includeOriginalTrigger)); + + return rootJ; + } + + void dataFromJson(json_t* rootJ) override { + json_t* includeOriginalTriggerJ = json_object_get(rootJ, "includeOriginalTrigger"); + if (includeOriginalTriggerJ) { + includeOriginalTrigger = json_boolean_value(includeOriginalTriggerJ); + } + } +}; + + +struct BurstWidget : ModuleWidget { + BurstWidget(Burst* module) { + setModule(module); + setPanel(APP->window->loadSvg(asset::plugin(pluginInstance, "res/panels/Burst.svg"))); + + addChild(createWidget(Vec(15, 0))); + addChild(createWidget(Vec(15, 365))); + + addParam(createParam(mm2px(Vec(28.44228, 10.13642)), module, Burst::CYCLE_PARAM)); + addParam(createParam(mm2px(Vec(9.0322, 16.21467)), module, Burst::QUANTITY_PARAM)); + addParam(createParam(mm2px(Vec(28.43253, 29.6592)), module, Burst::TRIGGER_PARAM)); + addParam(createParam(mm2px(Vec(17.26197, 41.95461)), module, Burst::QUANTITY_CV_PARAM)); + addParam(createParam(mm2px(Vec(22.85243, 58.45676)), module, Burst::DISTRIBUTION_PARAM)); + addParam(createParam(mm2px(Vec(28.47229, 74.91607)), module, Burst::TIME_PARAM)); + addParam(createParam(mm2px(Vec(22.75115, 91.35201)), module, Burst::PROBABILITY_PARAM)); + + addInput(createInput(mm2px(Vec(2.02153, 42.27628)), module, Burst::QUANTITY_INPUT)); + addInput(createInput(mm2px(Vec(7.90118, 58.74959)), module, Burst::DISTRIBUTION_INPUT)); + addInput(createInput(mm2px(Vec(2.05023, 75.25163)), module, Burst::PING_INPUT)); + addInput(createInput(mm2px(Vec(13.7751, 75.23049)), module, Burst::TIME_INPUT)); + addInput(createInput(mm2px(Vec(7.89545, 91.66642)), module, Burst::PROBABILITY_INPUT)); + addInput(createInput(mm2px(Vec(1.11155, 109.30346)), module, Burst::TRIGGER_INPUT)); + + addOutput(createOutput(mm2px(Vec(11.07808, 109.30346)), module, Burst::TEMPO_OUTPUT)); + addOutput(createOutput(mm2px(Vec(21.08452, 109.32528)), module, Burst::EOC_OUTPUT)); + addOutput(createOutput(mm2px(Vec(31.01113, 109.30346)), module, Burst::OUT_OUTPUT)); + + addChild(createLight>(mm2px(Vec(14.03676, 9.98712)), module, Burst::QUANTITY_LIGHTS + 0)); + addChild(createLight>(mm2px(Vec(18.35846, 10.85879)), module, Burst::QUANTITY_LIGHTS + 1)); + addChild(createLight>(mm2px(Vec(22.05722, 13.31827)), module, Burst::QUANTITY_LIGHTS + 2)); + addChild(createLight>(mm2px(Vec(24.48707, 16.96393)), module, Burst::QUANTITY_LIGHTS + 3)); + addChild(createLight>(mm2px(Vec(25.38476, 21.2523)), module, Burst::QUANTITY_LIGHTS + 4)); + addChild(createLight>(mm2px(Vec(24.48707, 25.5354)), module, Burst::QUANTITY_LIGHTS + 5)); + addChild(createLight>(mm2px(Vec(22.05722, 29.16905)), module, Burst::QUANTITY_LIGHTS + 6)); + addChild(createLight>(mm2px(Vec(18.35846, 31.62236)), module, Burst::QUANTITY_LIGHTS + 7)); + addChild(createLight>(mm2px(Vec(14.03676, 32.48786)), module, Burst::QUANTITY_LIGHTS + 8)); + addChild(createLight>(mm2px(Vec(9.74323, 31.62236)), module, Burst::QUANTITY_LIGHTS + 9)); + addChild(createLight>(mm2px(Vec(6.10149, 29.16905)), module, Burst::QUANTITY_LIGHTS + 10)); + addChild(createLight>(mm2px(Vec(3.68523, 25.5354)), module, Burst::QUANTITY_LIGHTS + 11)); + addChild(createLight>(mm2px(Vec(2.85312, 21.2523)), module, Burst::QUANTITY_LIGHTS + 12)); + addChild(createLight>(mm2px(Vec(3.68523, 16.96393)), module, Burst::QUANTITY_LIGHTS + 13)); + addChild(createLight>(mm2px(Vec(6.10149, 13.31827)), module, Burst::QUANTITY_LIGHTS + 14)); + addChild(createLight>(mm2px(Vec(9.74323, 10.85879)), module, Burst::QUANTITY_LIGHTS + 15)); + addChild(createLight>(mm2px(Vec(14.18119, 104.2831)), module, Burst::TEMPO_LIGHT)); + addChild(createLight>(mm2px(Vec(24.14772, 104.2831)), module, Burst::EOC_LIGHT)); + addChild(createLight>(mm2px(Vec(34.11425, 104.2831)), module, Burst::OUT_LIGHT)); + } + + void appendContextMenu(Menu* menu) override { + Burst* module = dynamic_cast(this->module); + assert(module); + + menu->addChild(new MenuSeparator()); + menu->addChild(createBoolPtrMenuItem("Include original trigger in output", "", &module->includeOriginalTrigger)); + } +}; + + +Model* modelBurst = createModel("Burst"); + diff --git a/src/plugin.cpp b/src/plugin.cpp index 49dbfdc..9ded879 100644 --- a/src/plugin.cpp +++ b/src/plugin.cpp @@ -27,4 +27,5 @@ void init(rack::Plugin *p) { p->addModel(modelChannelStrip); p->addModel(modelPonyVCO); p->addModel(modelMotionMTR); + p->addModel(modelBurst); } diff --git a/src/plugin.hpp b/src/plugin.hpp index 441efcb..067e4ef 100644 --- a/src/plugin.hpp +++ b/src/plugin.hpp @@ -28,6 +28,7 @@ extern Model* modelNoisePlethora; extern Model* modelChannelStrip; extern Model* modelPonyVCO; extern Model* modelMotionMTR; +extern Model* modelBurst; struct Knurlie : SvgScrew { Knurlie() { @@ -221,6 +222,13 @@ struct BefacoSlidePotSmall : app::SvgSlider { } }; +struct Davies1900hWhiteKnobEndless : Davies1900hKnob { + Davies1900hWhiteKnobEndless() { + setSvg(Svg::load(asset::plugin(pluginInstance, "res/components/Davies1900hWhiteEndless.svg"))); + bg->setSvg(Svg::load(asset::plugin(pluginInstance, "res/components/Davies1900hWhiteEndless_bg.svg"))); + } +}; + inline int unsigned_modulo(int a, int b) { return ((a % b) + b) % b; }