From d3ad6c6732cee8a80197aa351dd9efef9c278d4c Mon Sep 17 00:00:00 2001 From: Ewan <915048+hemmer@users.noreply.github.com> Date: Fri, 22 Mar 2024 13:40:41 +0000 Subject: [PATCH] Add Burst, Voltio (#46) --- .gitignore | 3 +- CHANGELOG.md | 10 + README.md | 4 +- plugin.json | 27 +- res/components/Davies1900hWhiteEndless.svg | 76 + res/components/Davies1900hWhiteEndless_bg.svg | 24 + res/fonts/MISO-info.txt | 56 + res/fonts/miso.otf | Bin 0 -> 25024 bytes res/panels/Burst.svg | 1131 ++++++++++++++ res/panels/Voltio.svg | 1338 +++++++++++++++++ src/Burst.cpp | 349 +++++ src/ChowDSP.hpp | 55 +- src/PonyVCO.cpp | 318 ++-- src/Voltio.cpp | 94 ++ src/plugin.cpp | 2 + src/plugin.hpp | 17 + 16 files changed, 3311 insertions(+), 193 deletions(-) create mode 100644 res/components/Davies1900hWhiteEndless.svg create mode 100644 res/components/Davies1900hWhiteEndless_bg.svg create mode 100644 res/fonts/MISO-info.txt create mode 100644 res/fonts/miso.otf create mode 100644 res/panels/Burst.svg create mode 100644 res/panels/Voltio.svg create mode 100644 src/Burst.cpp create mode 100644 src/Voltio.cpp diff --git a/.gitignore b/.gitignore index 23ac7f1..b81bb8a 100644 --- a/.gitignore +++ b/.gitignore @@ -3,4 +3,5 @@ /plugin.dylib /plugin.dll /plugin.so -.DS_Store \ No newline at end of file +.DS_Store +/.vscode \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md index 8400b3b..a20bc04 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,15 @@ # Change Log +## v2.5.0 + * Burst + * Initial release + * Voltio + * Initial release + * PonyVCO + * Now polyphonic + * Misc + * Fix trigger inputs to follow Rack voltage standards (Kickall, Muxlicer, Rampage) + ## v2.4.1 * Rampage * Fix SIMD bug diff --git a/README.md b/README.md index b14a75c..2caa49b 100644 --- a/README.md +++ b/README.md @@ -28,4 +28,6 @@ We have tried to make the VCV implementations as authentic as possible, however * to limit the pulsewidth from 5% to 95% (hardware is full range) * to remove DC from the pulse waveform output (hardware contains DC for non-50% duty cycles) -* MotionMTR optionally doesn't use the 10V normalling on inputs if in audio mode to avoid acidentally adding unwanted DC to audio signals, see context menu. E.g. if you temporarily unpatch an audio source whilst using it it mixer mode, you get 10V DC suddenly and a nasty pop. \ No newline at end of file +* MotionMTR optionally doesn't use the 10V normalling on inputs if in audio mode to avoid acidentally adding unwanted DC to audio signals, see context menu. E.g. if you temporarily unpatch an audio source whilst using it it mixer mode, you get 10V DC suddenly and a nasty pop. + +* Burst hardware version version can also set the tempo by tapping the encoder, this is not possible in the VCV version. \ No newline at end of file diff --git a/plugin.json b/plugin.json index a001e22..bcfe2df 100644 --- a/plugin.json +++ b/plugin.json @@ -1,6 +1,6 @@ { "slug": "Befaco", - "version": "2.4.1", + "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,30 @@ "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" + ] + }, + { + "slug": "Voltio", + "name": "Voltio", + "description": "An accurate voltage source and precision adder.", + "manualUrl": "https://www.befaco.org/voltio/", + "modularGridUrl": "https://www.modulargrid.net/e/befaco-voltio", + "tags": [ + "Hardware clone", + "Polyphonic", + "Utility" + ] } ] } \ 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/fonts/MISO-info.txt b/res/fonts/MISO-info.txt new file mode 100644 index 0000000..dffe47f --- /dev/null +++ b/res/fonts/MISO-info.txt @@ -0,0 +1,56 @@ + M M I SSS OOO + MM MM I S S O O + M M M M I S O O + M M M I S O O + M M I S O O + M M I S S O O + M M I SSS OOO + +--------------------------------------- +MISO is an architectural lettering font +completed in 2006 by Mårten Nettelbladt. +--------------------------------------- +MISO is available in three weights +(Light, Regular, Bold) +in TrueType and OpenType format. +--------------------------------------- + + L I C E N S E I N F O R M A T I O N +--------------------------------------- +MISO is a free typeface. However, +there is one important limitation: + +MISO MUST ALWAYS REMAIN COMPLETELY FREE + +You can use MISO for personal and commercial work. +You can share MISO with your friends +as long as you include this text file. + +You must not sell MISO. +You must not charge someone else for using MISO. +You must not bundle MISO with a sold product. + +Use it, share it, but keep it free. +--------------------------------------- + +Mårten Nettelbladt +Omkrets arkitektur +www.omkrets.se + +Stockholm, Sweden +July 9th 2009 + +--------------------------------------- +If you have any comments about MISO +please let me know: +miso (a) omkrets.se +--------------------------------------- + +November 27th 2008 +Converted to OpenType by Torin Hill. + +June 24th 2007 +Some small adjustments + +October 23rd 2006 +Released \ No newline at end of file diff --git a/res/fonts/miso.otf b/res/fonts/miso.otf new file mode 100644 index 0000000000000000000000000000000000000000..2b0c62efb789b1f90b28fc549232015efb4ee1d0 GIT binary patch literal 25024 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 X$pSMJ_x7fp)7CFrziiI7Zfy2b literal 0 HcmV?d00001 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/res/panels/Voltio.svg b/res/panels/Voltio.svg new file mode 100644 index 0000000..df367fb --- /dev/null +++ b/res/panels/Voltio.svg @@ -0,0 +1,1338 @@ + + + + + + + + + + + + + + + + + + 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/ChowDSP.hpp b/src/ChowDSP.hpp index 873a4d9..4d7cd6d 100644 --- a/src/ChowDSP.hpp +++ b/src/ChowDSP.hpp @@ -225,7 +225,7 @@ typedef TBiquadFilter<> BiquadFilter; Currently uses an 2*N-th order Butterworth filter. source: https://github.com/jatinchowdhury18/ChowDSP-VCV/blob/master/src/shared/AAFilter.hpp */ -template +template class AAFilter { public: AAFilter() = default; @@ -255,10 +255,10 @@ class AAFilter { auto Qs = calculateButterQs(2 * N); for (int i = 0; i < N; ++i) - filters[i].setParameters(BiquadFilter::Type::LOWPASS, fc / (osRatio * sampleRate), Qs[i], 1.0f); + filters[i].setParameters(TBiquadFilter::Type::LOWPASS, fc / (osRatio * sampleRate), Qs[i], 1.0f); } - inline float process(float x) noexcept { + inline T process(T x) noexcept { for (int i = 0; i < N; ++i) x = filters[i].process(x); @@ -266,14 +266,16 @@ class AAFilter { } private: - BiquadFilter filters[N]; + TBiquadFilter filters[N]; }; + /** * Base class for oversampling of any order * source: https://github.com/jatinchowdhury18/ChowDSP-VCV/blob/master/src/shared/oversampling.hpp */ +template class BaseOversampling { public: BaseOversampling() = default; @@ -283,13 +285,13 @@ class BaseOversampling { virtual void reset(float /*baseSampleRate*/) = 0; /** Upsample a single input sample and update the oversampled buffer */ - virtual void upsample(float) noexcept = 0; + virtual void upsample(T) noexcept = 0; /** Output a downsampled output sample from the current oversampled buffer */ - virtual float downsample() noexcept = 0; + virtual T downsample() noexcept = 0; /** Returns a pointer to the oversampled buffer */ - virtual float* getOSBuffer() noexcept = 0; + virtual T* getOSBuffer() noexcept = 0; }; @@ -305,8 +307,8 @@ class BaseOversampling { float y = oversample.downsample(); @endcode */ -template -class Oversampling : public BaseOversampling { +template +class Oversampling : public BaseOversampling { public: Oversampling() = default; virtual ~Oversampling() {} @@ -317,7 +319,7 @@ class Oversampling : public BaseOversampling { std::fill(osBuffer, &osBuffer[ratio], 0.0f); } - inline void upsample(float x) noexcept override { + inline void upsample(T x) noexcept override { osBuffer[0] = ratio * x; std::fill(&osBuffer[1], &osBuffer[ratio], 0.0f); @@ -325,25 +327,26 @@ class Oversampling : public BaseOversampling { osBuffer[k] = aiFilter.process(osBuffer[k]); } - inline float downsample() noexcept override { - float y = 0.0f; + inline T downsample() noexcept override { + T y = 0.0f; for (int k = 0; k < ratio; k++) y = aaFilter.process(osBuffer[k]); return y; } - inline float* getOSBuffer() noexcept override { + inline T* getOSBuffer() noexcept override { return osBuffer; } - float osBuffer[ratio]; + T osBuffer[ratio]; private: - AAFilter aaFilter; // anti-aliasing filter - AAFilter aiFilter; // anti-imaging filter + AAFilter aaFilter; // anti-aliasing filter + AAFilter aiFilter; // anti-imaging filter }; +typedef Oversampling<1, 4, simd::float_4> OversamplingSIMD; /** @@ -362,7 +365,7 @@ class Oversampling : public BaseOversampling { source (modified): https://github.com/jatinchowdhury18/ChowDSP-VCV/blob/master/src/shared/VariableOversampling.hpp */ -template +template class VariableOversampling { public: VariableOversampling() = default; @@ -384,17 +387,17 @@ class VariableOversampling { } /** Upsample a single input sample and update the oversampled buffer */ - inline void upsample(float x) noexcept { + inline void upsample(T x) noexcept { oss[osIdx]->upsample(x); } /** Output a downsampled output sample from the current oversampled buffer */ - inline float downsample() noexcept { + inline T downsample() noexcept { return oss[osIdx]->downsample(); } /** Returns a pointer to the oversampled buffer */ - inline float* getOSBuffer() noexcept { + inline T* getOSBuffer() noexcept { return oss[osIdx]->getOSBuffer(); } @@ -411,12 +414,12 @@ class VariableOversampling { int osIdx = 0; - Oversampling < 1 << 0, filtN > os0; // 1x - Oversampling < 1 << 1, filtN > os1; // 2x - Oversampling < 1 << 2, filtN > os2; // 4x - Oversampling < 1 << 3, filtN > os3; // 8x - Oversampling < 1 << 4, filtN > os4; // 16x - BaseOversampling* oss[NumOS] = { &os0, &os1, &os2, &os3, &os4 }; + Oversampling < 1 << 0, filtN, T > os0; // 1x + Oversampling < 1 << 1, filtN, T > os1; // 2x + Oversampling < 1 << 2, filtN, T > os2; // 4x + Oversampling < 1 << 3, filtN, T > os3; // 8x + Oversampling < 1 << 4, filtN, T > os4; // 16x + BaseOversampling* oss[NumOS] = { &os0, &os1, &os2, &os3, &os4 }; }; } // namespace chowdsp diff --git a/src/PonyVCO.cpp b/src/PonyVCO.cpp index bc424b5..ad6f52c 100644 --- a/src/PonyVCO.cpp +++ b/src/PonyVCO.cpp @@ -1,6 +1,7 @@ #include "plugin.hpp" #include "ChowDSP.hpp" +using simd::float_4; // references: // * "REDUCING THE ALIASING OF NONLINEAR WAVESHAPING USING CONTINUOUS-TIME CONVOLUTION" (https://www.dafx.de/paper-archive/2016/dafxpapers/20-DAFx-16_paper_41-PN.pdf) @@ -8,46 +9,27 @@ // * https://ccrma.stanford.edu/~jatin/Notebooks/adaa.html // * Pony waveshape https://www.desmos.com/calculator/1kvahyl4ti +template class FoldStage1 { public: - float process(float x, float xt) { - float y; + T process(T x, T xt) { + T y = simd::ifelse(simd::abs(x - xPrev) < 1e-5, + f(0.5 * (xPrev + x), xt), + (F(x, xt) - F(xPrev, xt)) / (x - xPrev)); - if (fabs(x - xPrev) < 1e-5) { - y = f(0.5 * (xPrev + x), xt); - } - else { - y = (F(x, xt) - F(xPrev, xt)) / (x - xPrev); - } xPrev = x; return y; } // xt - threshold x - static float f(float x, float xt) { - if (x > xt) { - return +5 * xt - 4 * x; - } - else if (x < -xt) { - return -5 * xt - 4 * x; - } - else { - return x; - } + static T f(T x, T xt) { + return simd::ifelse(x > xt, +5 * xt - 4 * x, simd::ifelse(x < -xt, -5 * xt - 4 * x, x)); } - static float F(float x, float xt) { - if (x > xt) { - return 5 * xt * x - 2 * x * x - 2.5 * xt * xt; - } - else if (x < -xt) { - return -5 * xt * x - 2 * x * x - 2.5 * xt * xt; - - } - else { - return x * x / 2.f; - } + static T F(T x, T xt) { + return simd::ifelse(x > xt, 5 * xt * x - 2 * x * x - 2.5 * xt * xt, + simd::ifelse(x < -xt, -5 * xt * x - 2 * x * x - 2.5 * xt * xt, x * x / 2.f)); } void reset() { @@ -55,55 +37,29 @@ class FoldStage1 { } private: - float xPrev = 0.f; + T xPrev = 0.f; }; +template class FoldStage2 { public: - float process(float x) { - float y; - - if (fabs(x - xPrev) < 1e-5) { - y = f(0.5 * (xPrev + x)); - } - else { - y = (F(x) - F(xPrev)) / (x - xPrev); - } + T process(T x) { + const T y = simd::ifelse(simd::abs(x - xPrev) < 1e-5, f(0.5 * (xPrev + x)), (F(x) - F(xPrev)) / (x - xPrev)); xPrev = x; return y; } - static float f(float x) { - if (-(x + 2) > c) { - return c; - } - else if (x < -1) { - return -(x + 2); - } - else if (x < 1) { - return x; - } - else if (-x + 2 > -c) { - return -x + 2; - } - else { - return -c; - } + static T f(T x) { + return simd::ifelse(-(x + 2) > c, c, simd::ifelse(x < -1, -(x + 2), simd::ifelse(x < 1, x, simd::ifelse(-x + 2 > -c, -x + 2, -c)))); } - static float F(float x) { - if (x < 0) { - return F(-x); - } - else if (x < 1) { - return x * x * 0.5; - } - else if (x < 2 + c) { - return 2 * x * (1.f - x * 0.25f) - 1.f; - } - else { - return 2 * (2 + c) * (1 - (2 + c) * 0.25f) - 1.f - c * (x - 2 - c); - } + static T F(T x) { + return simd::ifelse(x > 0, F_signed(x), F_signed(-x)); + } + + static T F_signed(T x) { + return simd::ifelse(x < 1, x * x * 0.5, simd::ifelse(x < 2.f + c, 2.f * x * (1.f - x * 0.25f) - 1.f, + 2.f * (2.f + c) * (1.f - (2.f + c) * 0.25f) - 1.f - c * (x - 2.f - c))); } void reset() { @@ -111,8 +67,8 @@ class FoldStage2 { } private: - float xPrev = 0.f; - static constexpr float c = 0.1; + T xPrev = 0.f; + static constexpr float c = 0.1f; }; @@ -148,10 +104,10 @@ struct PonyVCO : Module { }; float range[4] = {8.f, 1.f, 1.f / 12.f, 10.f}; - chowdsp::VariableOversampling<6> oversampler; // uses a 2*6=12th order Butterworth filter + chowdsp::VariableOversampling<6, float_4> oversampler[4]; // uses a 2*6=12th order Butterworth filter int oversamplingIndex = 1; // default is 2^oversamplingIndex == x2 oversampling - dsp::RCFilter blockTZFMDCFilter; + dsp::TRCFilter blockTZFMDCFilter[4]; bool blockTZFMDC = true; // hardware doesn't limit PW but some user might want to (to 5%->95%) @@ -160,10 +116,10 @@ struct PonyVCO : Module { // hardware has DC for non-50% duty cycle, optionally add/remove it bool removePulseDC = true; - dsp::SchmittTrigger syncTrigger; + dsp::TSchmittTrigger syncTrigger[4]; - FoldStage1 stage1; - FoldStage2 stage2; + FoldStage1 stage1[4]; + FoldStage2 stage2[4]; PonyVCO() { config(PARAMS_LEN, INPUTS_LEN, OUTPUTS_LEN, LIGHTS_LEN); @@ -191,22 +147,21 @@ struct PonyVCO : Module { void onSampleRateChange() override { float sampleRate = APP->engine->getSampleRate(); - blockTZFMDCFilter.setCutoffFreq(5.0 / sampleRate); - oversampler.setOversamplingIndex(oversamplingIndex); - oversampler.reset(sampleRate); + for (int c = 0; c < 4; c++) { + blockTZFMDCFilter[c].setCutoffFreq(5.0 / sampleRate); + oversampler[c].setOversamplingIndex(oversamplingIndex); + oversampler[c].reset(sampleRate); - stage1.reset(); - stage2.reset(); + stage1[c].reset(); + stage2[c].reset(); + } } // implementation taken from "Alias-Suppressed Oscillators Based on Differentiated Polynomial Waveforms", // also the notes from Surge Synthesier repo: // https://github.com/surge-synthesizer/surge/blob/09f1ec8e103265bef6fc0d8a0fc188238197bf8c/src/common/dsp/oscillators/ModernOscillator.cpp#L19 - // Calculation is performed at double precision, as the differencing equations appeared to work poorly with only float. - double phase = 0.0; // phase at current (sub)sample - double phases[3] = {}; // phase as extrapolated to the current and two previous samples - double sawBuffer[3] = {}, sawOffsetBuff[3] = {}, triBuffer[3] = {}; // buffers for storing the terms in the difference equation + float_4 phase[4] = {}; // phase at current (sub)sample void process(const ProcessArgs& args) override { @@ -216,130 +171,160 @@ struct PonyVCO : Module { const Waveform waveform = (Waveform) params[WAVE_PARAM].getValue(); const float mult = lfoMode ? 1.0 : dsp::FREQ_C4; const float baseFreq = std::pow(2, (int)(params[OCT_PARAM].getValue() - 3)) * mult; - const int oversamplingRatio = lfoMode ? 1 : oversampler.getOversamplingRatio(); - const float timbre = clamp(params[TIMBRE_PARAM].getValue() + inputs[TIMBRE_INPUT].getVoltage() / 10.f, 0.f, 1.f); - - float tzfmVoltage = inputs[TZFM_INPUT].getVoltage(); - if (blockTZFMDC) { - blockTZFMDCFilter.process(tzfmVoltage); - tzfmVoltage = blockTZFMDCFilter.highpass(); - } + const int oversamplingRatio = lfoMode ? 1 : oversampler[0].getOversamplingRatio(); - const double pitch = inputs[VOCT_INPUT].getVoltage() + params[FREQ_PARAM].getValue() * range[rangeIndex]; - const double freq = baseFreq * simd::pow(2.f, pitch); - const double deltaBasePhase = clamp(freq * args.sampleTime / oversamplingRatio, -0.5f, 0.5f); - // denominator for the second-order FD - const double denominator = 0.25 / (deltaBasePhase * deltaBasePhase); - // not clamped, but _total_ phase treated later with floor/ceil - const double deltaFMPhase = freq * tzfmVoltage * args.sampleTime / oversamplingRatio; - - float pw = timbre; - if (limitPW) { - pw = clamp(pw, 0.05, 0.95); - } - // pulsewave waveform doesn't have DC even for non 50% duty cycles, but Befaco team would like the option - // for it to be added back in for hardware compatibility reasons - const float pulseDCOffset = (!removePulseDC) * 2.f * (0.5f - pw); - - // hard sync - if (syncTrigger.process(inputs[SYNC_INPUT].getVoltage())) { - // hardware waveform is actually cos, so pi/2 phase offset is required - // - variable phase is defined on [0, 1] rather than [0, 2pi] so pi/2 -> 0.25 - phase = (waveform == WAVE_SIN) ? 0.25f : 0.f; - } + // number of active polyphony engines (must be at least 1) + const int channels = std::max({inputs[TZFM_INPUT].getChannels(), inputs[VOCT_INPUT].getChannels(), inputs[TIMBRE_INPUT].getChannels(), 1}); - float* osBuffer = oversampler.getOSBuffer(); - for (int i = 0; i < oversamplingRatio; ++i) { + for (int c = 0; c < channels; c += 4) { + const float_4 timbre = simd::clamp(params[TIMBRE_PARAM].getValue() + inputs[TIMBRE_INPUT].getPolyVoltageSimd(c) / 10.f, 0.f, 1.f); - phase += deltaBasePhase + deltaFMPhase; - if (phase > 1.f) { - phase -= floor(phase); + float_4 tzfmVoltage = inputs[TZFM_INPUT].getPolyVoltageSimd(c); + if (blockTZFMDC) { + blockTZFMDCFilter[c / 4].process(tzfmVoltage); + tzfmVoltage = blockTZFMDCFilter[c / 4].highpass(); } - else if (phase < 0.f) { - phase += -ceil(phase) + 1; + + const float_4 pitch = inputs[VOCT_INPUT].getPolyVoltageSimd(c) + params[FREQ_PARAM].getValue() * range[rangeIndex]; + const float_4 freq = baseFreq * simd::pow(2.f, pitch); + const float_4 deltaBasePhase = simd::clamp(freq * args.sampleTime / oversamplingRatio, -0.5f, 0.5f); + // floating point arithmetic doesn't work well at low frequencies, specifically because the finite difference denominator + // becomes tiny - we check for that scenario and use naive / 1st order waveforms in that frequency regime (as aliasing isn't + // a problem there). With no oversampling, at 44100Hz, the threshold frequency is 44.1Hz. + const float_4 lowFreqRegime = simd::abs(deltaBasePhase) < 1e-3; + + // 1 / denominator for the second-order FD + const float_4 denominatorInv = 0.25 / (deltaBasePhase * deltaBasePhase); + // not clamped, but _total_ phase treated later with floor/ceil + const float_4 deltaFMPhase = freq * tzfmVoltage * args.sampleTime / oversamplingRatio; + + float_4 pw = timbre; + if (limitPW) { + pw = clamp(pw, 0.05, 0.95); } + // pulsewave waveform doesn't have DC even for non 50% duty cycles, but Befaco team would like the option + // for it to be added back in for hardware compatibility reasons + const float_4 pulseDCOffset = (!removePulseDC) * 2.f * (0.5f - pw); - // sin is simple + // hard sync + const float_4 syncMask = syncTrigger[c / 4].process(inputs[SYNC_INPUT].getPolyVoltageSimd(c)); if (waveform == WAVE_SIN) { - osBuffer[i] = sin2pi_pade_05_5_4(phase); + // hardware waveform is actually cos, so pi/2 phase offset is required + // - variable phase is defined on [0, 1] rather than [0, 2pi] so pi/2 -> 0.25 + phase[c / 4] = simd::ifelse(syncMask, 0.25f, phase[c / 4]); } else { + phase[c / 4] = simd::ifelse(syncMask, 0.f, phase[c / 4]); + } - phases[0] = phase - 2 * deltaBasePhase + (phase < 2 * deltaBasePhase); - phases[1] = phase - deltaBasePhase + (phase < deltaBasePhase); - phases[2] = phase; + float_4* osBuffer = oversampler[c / 4].getOSBuffer(); + for (int i = 0; i < oversamplingRatio; ++i) { - switch (waveform) { - case WAVE_TRI: { - osBuffer[i] = aliasSuppressedTri() * denominator; - break; - } - case WAVE_SAW: { - osBuffer[i] = aliasSuppressedSaw() * denominator; - break; - } - case WAVE_PULSE: { - double saw = aliasSuppressedSaw(); - double sawOffset = aliasSuppressedOffsetSaw(pw); + phase[c / 4] += deltaBasePhase + deltaFMPhase; + // ensure within [0, 1] + phase[c / 4] -= simd::floor(phase[c / 4]); - osBuffer[i] = (sawOffset - saw) * denominator; - osBuffer[i] += pulseDCOffset; - break; + // sin is simple + if (waveform == WAVE_SIN) { + osBuffer[i] = sin2pi_pade_05_5_4(phase[c / 4]); + } + else { + float_4 phases[3]; // phase as extrapolated to the current and two previous samples + + phases[0] = phase[c / 4] - 2 * deltaBasePhase + simd::ifelse(phase[c / 4] < 2 * deltaBasePhase, 1.f, 0.f); + phases[1] = phase[c / 4] - deltaBasePhase + simd::ifelse(phase[c / 4] < deltaBasePhase, 1.f, 0.f); + phases[2] = phase[c / 4]; + + switch (waveform) { + case WAVE_TRI: { + const float_4 dpwOrder1 = 1.0 - 2.0 * simd::abs(2 * phase[c / 4] - 1.0); + const float_4 dpwOrder3 = aliasSuppressedTri(phases) * denominatorInv; + + osBuffer[i] = simd::ifelse(lowFreqRegime, dpwOrder1, dpwOrder3); + break; + } + case WAVE_SAW: { + const float_4 dpwOrder1 = 2 * phase[c / 4] - 1.0; + const float_4 dpwOrder3 = aliasSuppressedSaw(phases) * denominatorInv; + + osBuffer[i] = simd::ifelse(lowFreqRegime, dpwOrder1, dpwOrder3); + break; + } + case WAVE_PULSE: { + float_4 dpwOrder1 = simd::ifelse(phase[c / 4] < 1. - pw, +1.0, -1.0); + dpwOrder1 -= removePulseDC ? 2.f * (0.5f - pw) : 0.f; + + float_4 saw = aliasSuppressedSaw(phases); + float_4 sawOffset = aliasSuppressedOffsetSaw(phases, pw); + float_4 dpwOrder3 = (sawOffset - saw) * denominatorInv + pulseDCOffset; + + osBuffer[i] = simd::ifelse(lowFreqRegime, dpwOrder1, dpwOrder3); + break; + } + default: break; } - default: break; } - } - if (waveform != WAVE_PULSE) { - osBuffer[i] = wavefolder(osBuffer[i], (1 - 0.85 * timbre)); - } - } + if (waveform != WAVE_PULSE) { + osBuffer[i] = wavefolder(osBuffer[i], (1 - 0.85 * timbre), c); + } + + } // end of oversampling loop - // downsample (if required) - const float out = (oversamplingRatio > 1) ? oversampler.downsample() : osBuffer[0]; + // downsample (if required) + const float_4 out = (oversamplingRatio > 1) ? oversampler[c / 4].downsample() : osBuffer[0]; - // end of chain VCA - const float gain = std::max(0.f, inputs[VCA_INPUT].getNormalVoltage(10.f) / 10.f); - outputs[OUT_OUTPUT].setVoltage(5.f * out * gain); + // end of chain VCA + const float_4 gain = simd::clamp(inputs[VCA_INPUT].getNormalPolyVoltageSimd(10.f, c) / 10.f, 0.f, 1.f); + outputs[OUT_OUTPUT].setVoltageSimd(5.f * out * gain, c); + + } // end of channels loop + + outputs[OUT_OUTPUT].setChannels(channels); } - double aliasSuppressedTri() { + float_4 aliasSuppressedTri(float_4* phases) { + float_4 triBuffer[3]; for (int i = 0; i < 3; ++i) { - double p = 2 * phases[i] - 1.0; // range -1.0 to +1.0 - double s = 0.5 - std::abs(p); // eq 30 + float_4 p = 2 * phases[i] - 1.0; // range -1.0 to +1.0 + float_4 s = 0.5 - simd::abs(p); // eq 30 triBuffer[i] = (s * s * s - 0.75 * s) / 3.0; // eq 29 } return (triBuffer[0] - 2.0 * triBuffer[1] + triBuffer[2]); } - double aliasSuppressedSaw() { + float_4 aliasSuppressedSaw(float_4* phases) { + float_4 sawBuffer[3]; for (int i = 0; i < 3; ++i) { - double p = 2 * phases[i] - 1.0; // range -1 to +1 + float_4 p = 2 * phases[i] - 1.0; // range -1 to +1 sawBuffer[i] = (p * p * p - p) / 6.0; // eq 11 } return (sawBuffer[0] - 2.0 * sawBuffer[1] + sawBuffer[2]); } - double aliasSuppressedOffsetSaw(double pw) { + float_4 aliasSuppressedOffsetSaw(float_4* phases, float_4 pw) { + float_4 sawOffsetBuff[3]; + for (int i = 0; i < 3; ++i) { - double p = 2 * phases[i] - 1.0; // range -1 to +1 - double pwp = p + 2 * pw; // phase after pw (pw in [0, 1]) - pwp += (pwp > 1) * -2; // modulo on [-1, +1] + float_4 p = 2 * phases[i] - 1.0; // range -1 to +1 + float_4 pwp = p + 2 * pw; // phase after pw (pw in [0, 1]) + pwp += simd::ifelse(pwp > 1, -2, 0); // modulo on [-1, +1] sawOffsetBuff[i] = (pwp * pwp * pwp - pwp) / 6.0; // eq 11 } return (sawOffsetBuff[0] - 2.0 * sawOffsetBuff[1] + sawOffsetBuff[2]); } - float wavefolder(float x, float xt) { - return stage2.process(stage1.process(x, xt)); + float_4 wavefolder(float_4 x, float_4 xt, int c) { + return stage2[c / 4].process(stage1[c / 4].process(x, xt)); } json_t* dataToJson() override { json_t* rootJ = json_object(); json_object_set_new(rootJ, "blockTZFMDC", json_boolean(blockTZFMDC)); json_object_set_new(rootJ, "removePulseDC", json_boolean(removePulseDC)); - json_object_set_new(rootJ, "oversamplingIndex", json_integer(oversampler.getOversamplingIndex())); + json_object_set_new(rootJ, "limitPW", json_boolean(limitPW)); + json_object_set_new(rootJ, "oversamplingIndex", json_integer(oversampler[0].getOversamplingIndex())); return rootJ; } @@ -355,6 +340,11 @@ struct PonyVCO : Module { removePulseDC = json_boolean_value(removePulseDCJ); } + json_t* limitPWJ = json_object_get(rootJ, "limitPW"); + if (limitPWJ) { + limitPW = json_boolean_value(limitPWJ); + } + json_t* oversamplingIndexJ = json_object_get(rootJ, "oversamplingIndex"); if (oversamplingIndexJ) { oversamplingIndex = json_integer_value(oversamplingIndexJ); diff --git a/src/Voltio.cpp b/src/Voltio.cpp new file mode 100644 index 0000000..b25df3b --- /dev/null +++ b/src/Voltio.cpp @@ -0,0 +1,94 @@ +#include "plugin.hpp" + +using simd::float_4; + +struct Davies1900hLargeLightGreyKnobCustom : Davies1900hLargeLightGreyKnob { + widget::SvgWidget* bg; + + Davies1900hLargeLightGreyKnobCustom() { + minAngle = -0.83 * M_PI; + maxAngle = M_PI; + + bg = new widget::SvgWidget; + fb->addChildBelow(bg, tw); + } +}; + +struct Voltio : Module { + enum ParamId { + OCT_PARAM, + RANGE_PARAM, + SEMITONES_PARAM, + PARAMS_LEN + }; + enum InputId { + SUM_INPUT, + INPUTS_LEN + }; + enum OutputId { + OUT_OUTPUT, + OUTPUTS_LEN + }; + enum LightId { + PLUSMINUS5_LIGHT, + ZEROTOTEN_LIGHT, + LIGHTS_LEN + }; + + Voltio() { + config(PARAMS_LEN, INPUTS_LEN, OUTPUTS_LEN, LIGHTS_LEN); + auto octParam = configParam(OCT_PARAM, 0.f, 10.f, 0.f, "Octave"); + octParam->snapEnabled = true; + + configSwitch(RANGE_PARAM, 0.f, 1.f, 0.f, "Range", {"-5 to +5", "0 to 10"}); + auto semitonesParam = configParam(SEMITONES_PARAM, 0.f, 11.f, 0.f, "Semitones"); + semitonesParam->snapEnabled = true; + + configInput(SUM_INPUT, "Sum"); + configOutput(OUT_OUTPUT, ""); + } + + void process(const ProcessArgs& args) override { + const int channels = std::max(1, inputs[SUM_INPUT].getChannels()); + + for (int c = 0; c < channels; c += 4) { + float_4 in = inputs[SUM_INPUT].getPolyVoltageSimd(c); + + float offset = params[RANGE_PARAM].getValue() ? -5.f : 0.f; + in += params[SEMITONES_PARAM].getValue() / 12.f + params[OCT_PARAM].getValue() + offset; + + outputs[OUT_OUTPUT].setVoltageSimd(in, c); + } + + outputs[OUT_OUTPUT].setChannels(channels); + + lights[PLUSMINUS5_LIGHT].setBrightness(params[RANGE_PARAM].getValue() ? 1.f : 0.f); + lights[ZEROTOTEN_LIGHT].setBrightness(params[RANGE_PARAM].getValue() ? 0.f : 1.f); + } + +}; + + +struct VoltioWidget : ModuleWidget { + VoltioWidget(Voltio* module) { + setModule(module); + setPanel(createPanel(asset::plugin(pluginInstance, "res/panels/Voltio.svg"))); + + addChild(createWidget(Vec(RACK_GRID_WIDTH, 0))); + addChild(createWidget(Vec(RACK_GRID_WIDTH, RACK_GRID_HEIGHT - RACK_GRID_WIDTH))); + + addParam(createParamCentered(mm2px(Vec(15.0, 20.828)), module, Voltio::OCT_PARAM)); + addParam(createParamCentered(mm2px(Vec(22.083, 44.061)), module, Voltio::RANGE_PARAM)); + addParam(createParamCentered(mm2px(Vec(15.0, 67.275)), module, Voltio::SEMITONES_PARAM)); + + addInput(createInputCentered(mm2px(Vec(7.117, 111.003)), module, Voltio::SUM_INPUT)); + + addOutput(createOutputCentered(mm2px(Vec(22.661, 111.003)), module, Voltio::OUT_OUTPUT)); + + addChild(createLightCentered>(mm2px(Vec(5.695, 41.541)), module, Voltio::PLUSMINUS5_LIGHT)); + addChild(createLightCentered>(mm2px(Vec(5.695, 46.633)), module, Voltio::ZEROTOTEN_LIGHT)); + } +}; + + +Model* modelVoltio = createModel("Voltio"); \ No newline at end of file diff --git a/src/plugin.cpp b/src/plugin.cpp index 49dbfdc..475f31e 100644 --- a/src/plugin.cpp +++ b/src/plugin.cpp @@ -27,4 +27,6 @@ void init(rack::Plugin *p) { p->addModel(modelChannelStrip); p->addModel(modelPonyVCO); p->addModel(modelMotionMTR); + p->addModel(modelBurst); + p->addModel(modelVoltio); } diff --git a/src/plugin.hpp b/src/plugin.hpp index 441efcb..f9d86e8 100644 --- a/src/plugin.hpp +++ b/src/plugin.hpp @@ -28,6 +28,8 @@ extern Model* modelNoisePlethora; extern Model* modelChannelStrip; extern Model* modelPonyVCO; extern Model* modelMotionMTR; +extern Model* modelBurst; +extern Model* modelVoltio; struct Knurlie : SvgScrew { Knurlie() { @@ -221,6 +223,21 @@ struct BefacoSlidePotSmall : app::SvgSlider { } }; +struct BefacoButton : app::SvgSwitch { + BefacoButton() { + momentary = true; + addFrame(APP->window->loadSvg(asset::plugin(pluginInstance, "res/components/BefacoButton_0.svg"))); + addFrame(APP->window->loadSvg(asset::plugin(pluginInstance, "res/components/BefacoButton_1.svg"))); + } +}; + +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; }