#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, 4, "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");