From 20348771616b61a9d9b08e75cc641c792fb42bc5 Mon Sep 17 00:00:00 2001
From: hemmer <915048+hemmer@users.noreply.github.com>
Date: Tue, 30 May 2023 21:55:58 +0100
Subject: [PATCH] Added initial prototype of Burst
---
CHANGELOG.md | 6 +
plugin.json | 15 +-
res/components/Davies1900hWhiteEndless.svg | 76 ++
res/components/Davies1900hWhiteEndless_bg.svg | 24 +
res/panels/Burst.svg | 1131 +++++++++++++++++
src/Burst.cpp | 349 +++++
src/plugin.cpp | 1 +
src/plugin.hpp | 8 +
8 files changed, 1609 insertions(+), 1 deletion(-)
create mode 100644 res/components/Davies1900hWhiteEndless.svg
create mode 100644 res/components/Davies1900hWhiteEndless_bg.svg
create mode 100644 res/panels/Burst.svg
create mode 100644 src/Burst.cpp
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 @@
+
+
+
+
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;
}