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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
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Â
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Lienzo 1
+
+ Capa 1
+
+
+
+
+
+
+
+ Capa 1
+
+
+
+
+
+
+
+
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 @@ public:
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 @@ public:
}
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 @@ public:
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 @@ public:
float y = oversample.downsample();
@endcode
*/
-template
-class Oversampling : public BaseOversampling {
+template
+class Oversampling : public BaseOversampling {
public:
Oversampling() = default;
virtual ~Oversampling() {}
@@ -317,7 +319,7 @@ public:
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 @@ public:
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 @@ private:
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 @@ public:
}
/** 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 @@ private:
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 @@ public:
}
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 @@ public:
}
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;
}
From c73fb6895c78e452620ba7101da091d3b6a7a494 Mon Sep 17 00:00:00 2001
From: Ewan <915048+hemmer@users.noreply.github.com>
Date: Thu, 4 Apr 2024 20:45:51 +0100
Subject: [PATCH 2/4] v2.6.0 Octaves (#47)
Better defaults for ADSR, Burst
Fix Voltio label bug
---
CHANGELOG.md | 8 +
plugin.json | 13 +-
res/panels/Octaves.svg | 2383 ++++++++++++++++++++++++++++++++++++++++
src/ADSR.cpp | 8 +-
src/Burst.cpp | 2 +-
src/Octaves.cpp | 383 +++++++
src/Voltio.cpp | 2 +-
src/plugin.cpp | 1 +
src/plugin.hpp | 11 +-
9 files changed, 2799 insertions(+), 12 deletions(-)
create mode 100644 res/panels/Octaves.svg
create mode 100644 src/Octaves.cpp
diff --git a/CHANGELOG.md b/CHANGELOG.md
index a20bc04..58da59d 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +1,13 @@
# Change Log
+
+## v2.6.0
+ * Octaves
+ * Initial release
+ * Misc
+ * Better default values for ADSR and Burst
+
+
## v2.5.0
* Burst
* Initial release
diff --git a/plugin.json b/plugin.json
index bcfe2df..3befb93 100644
--- a/plugin.json
+++ b/plugin.json
@@ -1,6 +1,6 @@
{
"slug": "Befaco",
- "version": "2.5.0",
+ "version": "2.6.0",
"license": "GPL-3.0-or-later",
"name": "Befaco",
"brand": "Befaco",
@@ -307,6 +307,17 @@
"Polyphonic",
"Utility"
]
+ },
+ {
+ "slug": "Octaves",
+ "name": "Octaves",
+ "description": "A harsh and funky take of an additive Oscillator.",
+ "manualUrl": "https://www.befaco.org/octaves-vco/",
+ "modularGridUrl": "https://www.modulargrid.net/e/befaco-octaves-vco",
+ "tags": [
+ "Hardware clone",
+ "VCO"
+ ]
}
]
}
\ No newline at end of file
diff --git a/res/panels/Octaves.svg b/res/panels/Octaves.svg
new file mode 100644
index 0000000..2f99793
--- /dev/null
+++ b/res/panels/Octaves.svg
@@ -0,0 +1,2383 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ image/svg+xml
+
+
+
+
+
+
+
+
+
+ Â Â
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Â
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/ADSR.cpp b/src/ADSR.cpp
index b65b756..5a12f5d 100644
--- a/src/ADSR.cpp
+++ b/src/ADSR.cpp
@@ -231,10 +231,10 @@ struct ADSR : Module {
configButton(MANUAL_TRIGGER_PARAM, "Trigger envelope");
configParam(SHAPE_PARAM, 0.f, 1.f, 0.f, "Envelope shape");
- configParam(ATTACK_PARAM, 0.f, 1.f, 0.f, "Attack time", "s", maxStageTime / minStageTime, minStageTime);
- configParam(DECAY_PARAM, 0.f, 1.f, 0.f, "Decay time", "s", maxStageTime / minStageTime, minStageTime);
- configParam(SUSTAIN_PARAM, 0.f, 1.f, 0.f, "Sustain level", "%", 0.f, 100.f);
- configParam(RELEASE_PARAM, 0.f, 1.f, 0.f, "Release time", "s", maxStageTime / minStageTime, minStageTime);
+ configParam(ATTACK_PARAM, 0.f, 1.f, 0.4f, "Attack time", "s", maxStageTime / minStageTime, minStageTime);
+ configParam(DECAY_PARAM, 0.f, 1.f, 0.4f, "Decay time", "s", maxStageTime / minStageTime, minStageTime);
+ configParam(SUSTAIN_PARAM, 0.f, 1.f, 0.5f, "Sustain level", "%", 0.f, 100.f);
+ configParam(RELEASE_PARAM, 0.f, 1.f, 0.4f, "Release time", "s", maxStageTime / minStageTime, minStageTime);
configInput(TRIGGER_INPUT, "Trigger");
configInput(CV_ATTACK_INPUT, "Attack CV");
diff --git a/src/Burst.cpp b/src/Burst.cpp
index f8108ab..912609c 100644
--- a/src/Burst.cpp
+++ b/src/Burst.cpp
@@ -183,7 +183,7 @@ struct Burst : Module {
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");
+ 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");
diff --git a/src/Octaves.cpp b/src/Octaves.cpp
new file mode 100644
index 0000000..e0723d3
--- /dev/null
+++ b/src/Octaves.cpp
@@ -0,0 +1,383 @@
+#include "plugin.hpp"
+#include "ChowDSP.hpp"
+
+using namespace simd;
+
+float aliasSuppressedSaw(const float* phases, float pw) {
+ float sawBuffer[3];
+ for (int i = 0; i < 3; ++i) {
+ float p = 2 * phases[i] - 1.0; // range -1 to +1
+ float pwp = p + 2 * pw; // phase after pw (pw in [0, 1])
+ pwp += simd::ifelse(pwp > 1, -2, simd::ifelse(pwp < -1, +2, 0)); // modulo on [-1, +1]
+ sawBuffer[i] = (pwp * pwp * pwp - pwp) / 6.0; // eq 11
+ }
+
+ return (sawBuffer[0] - 2.0 * sawBuffer[1] + sawBuffer[2]);
+}
+
+float aliasSuppressedOffsetSaw(const float* phases, float pw) {
+ float sawOffsetBuff[3];
+
+ for (int i = 0; i < 3; ++i) {
+ float pwp = 2 * phases[i] - 2 * pw; // range -1 to +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]);
+}
+
+template
+class HardClipperADAA {
+public:
+
+ T process(T x) {
+ 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 T f(T x) {
+ return simd::ifelse(simd::abs(x) < 1, x, simd::sgn(x));
+ }
+
+ static T F(T x) {
+ return simd::ifelse(simd::abs(x) < 1, 0.5 * x * x, x * simd::sgn(x) - 0.5);
+ }
+
+ void reset() {
+ xPrev = 0.f;
+ }
+
+private:
+ T xPrev = 0.f;
+};
+
+struct Octaves : Module {
+ enum ParamId {
+ PWM_CV_PARAM,
+ OCTAVE_PARAM,
+ TUNE_PARAM,
+ PWM_PARAM,
+ RANGE_PARAM,
+ GAIN_01F_PARAM,
+ GAIN_02F_PARAM,
+ GAIN_04F_PARAM,
+ GAIN_08F_PARAM,
+ GAIN_16F_PARAM,
+ GAIN_32F_PARAM,
+ PARAMS_LEN
+ };
+ enum InputId {
+ VOCT1_INPUT,
+ VOCT2_INPUT,
+ SYNC_INPUT,
+ PWM_INPUT,
+ GAIN_01F_INPUT,
+ GAIN_02F_INPUT,
+ GAIN_04F_INPUT,
+ GAIN_08F_INPUT,
+ GAIN_16F_INPUT,
+ GAIN_32F_INPUT,
+ INPUTS_LEN
+ };
+ enum OutputId {
+ OUT_01F_OUTPUT,
+ OUT_02F_OUTPUT,
+ OUT_04F_OUTPUT,
+ OUT_08F_OUTPUT,
+ OUT_16F_OUTPUT,
+ OUT_32F_OUTPUT,
+ OUTPUTS_LEN
+ };
+ enum LightId {
+ LIGHTS_LEN
+ };
+
+ bool limitPW = true;
+ bool removePulseDC = false;
+ bool useTriangleCore = false;
+ static const int NUM_OUTPUTS = 6;
+ const float ranges[3] = {4.f, 1.f, 1.f / 12.f}; // full, octave, semitone
+
+ float_4 phase[4] = {}; // phase for core waveform, in [0, 1]
+ chowdsp::VariableOversampling<6, float_4> oversampler[NUM_OUTPUTS][4]; // uses a 2*6=12th order Butterworth filter
+ int oversamplingIndex = 1; // default is 2^oversamplingIndex == x2 oversampling
+
+ DCBlockerT<2, float_4> blockDCFilter[NUM_OUTPUTS][4]; // optionally block DC with RC filter @ ~22 Hz
+ dsp::TSchmittTrigger syncTrigger[4]; // for hard sync
+
+ Octaves() {
+ config(PARAMS_LEN, INPUTS_LEN, OUTPUTS_LEN, LIGHTS_LEN);
+ configParam(PWM_CV_PARAM, 0.f, 1.f, 1.f, "PWM CV attenuater");
+
+ auto octParam = configSwitch(OCTAVE_PARAM, 0.f, 6.f, 1.f, "Octave", {"C1", "C2", "C3", "C4", "C5", "C6", "C7"});
+ octParam->snapEnabled = true;
+
+ configParam(TUNE_PARAM, -1.f, 1.f, 0.f, "Tune");
+ configParam(PWM_PARAM, 0.5f, 0.f, 0.5f, "PWM");
+ auto rangeParam = configSwitch(RANGE_PARAM, 0.f, 2.f, 1.f, "Range", {"VCO: Full", "VCO: Octave", "VCO: Semitone"});
+ rangeParam->snapEnabled = true;
+
+ configParam(GAIN_01F_PARAM, 0.f, 1.f, 1.00f, "Gain Fundamental");
+ configParam(GAIN_02F_PARAM, 0.f, 1.f, 0.75f, "Gain x2 Fundamental");
+ configParam(GAIN_04F_PARAM, 0.f, 1.f, 0.50f, "Gain x4 Fundamental");
+ configParam(GAIN_08F_PARAM, 0.f, 1.f, 0.25f, "Gain x8 Fundamental");
+ configParam(GAIN_16F_PARAM, 0.f, 1.f, 0.f, "Gain x16 Fundamental");
+ configParam(GAIN_32F_PARAM, 0.f, 1.f, 0.f, "Gain x32 Fundamental");
+
+ configInput(VOCT1_INPUT, "V/Octave 1");
+ configInput(VOCT2_INPUT, "V/Octave 2");
+ configInput(SYNC_INPUT, "Sync");
+ configInput(PWM_INPUT, "PWM");
+ configInput(GAIN_01F_INPUT, "Gain x1F CV");
+ configInput(GAIN_02F_INPUT, "Gain x1F CV");
+ configInput(GAIN_04F_INPUT, "Gain x1F CV");
+ configInput(GAIN_08F_INPUT, "Gain x1F CV");
+ configInput(GAIN_16F_INPUT, "Gain x1F CV");
+ configInput(GAIN_32F_INPUT, "Gain x1F CV");
+
+ configOutput(OUT_01F_OUTPUT, "x1F");
+ configOutput(OUT_02F_OUTPUT, "x2F");
+ configOutput(OUT_04F_OUTPUT, "x4F");
+ configOutput(OUT_08F_OUTPUT, "x8F");
+ configOutput(OUT_16F_OUTPUT, "x16F");
+ configOutput(OUT_32F_OUTPUT, "x32F");
+
+ // calculate up/downsampling rates
+ onSampleRateChange();
+ }
+
+ void onSampleRateChange() override {
+ float sampleRate = APP->engine->getSampleRate();
+ for (int c = 0; c < NUM_OUTPUTS; c++) {
+ for (int i = 0; i < 4; i++) {
+ oversampler[c][i].setOversamplingIndex(oversamplingIndex);
+ oversampler[c][i].reset(sampleRate);
+ blockDCFilter[c][i].setFrequency(22.05 / sampleRate);
+ }
+ }
+ }
+
+
+ void process(const ProcessArgs& args) override {
+
+ const int numActivePolyphonyEngines = getNumActivePolyphonyEngines();
+
+ // work out active outputs
+ const std::vector connectedOutputs = getConnectedOutputs();
+ if (connectedOutputs.size() == 0) {
+ return;
+ }
+ // only process up to highest active channel
+ const int highestOutput = *std::max_element(connectedOutputs.begin(), connectedOutputs.end());
+
+ for (int c = 0; c < numActivePolyphonyEngines; c += 4) {
+
+ const int rangeIndex = params[RANGE_PARAM].getValue();
+ float_4 pitch = ranges[rangeIndex] * params[TUNE_PARAM].getValue() + inputs[VOCT1_INPUT].getPolyVoltageSimd(c) + inputs[VOCT2_INPUT].getPolyVoltageSimd(c);
+ pitch += params[OCTAVE_PARAM].getValue() - 3;
+ const float_4 freq = dsp::FREQ_C4 * dsp::exp2_taylor5(pitch);
+ // -1 to +1
+ const float_4 pwmCV = params[PWM_CV_PARAM].getValue() * clamp(inputs[PWM_INPUT].getPolyVoltageSimd(c) / 10.f, -1.f, 1.f);
+ const float_4 pulseWidthLimit = limitPW ? 0.05f : 0.0f;
+
+ // pwm in [-0.25 : +0.25]
+ const float_4 pwm = 2 * clamp(0.5 - params[PWM_PARAM].getValue() + 0.5 * pwmCV, -0.5f + pulseWidthLimit, 0.5f - pulseWidthLimit);
+
+ const int oversamplingRatio = oversampler[0][0].getOversamplingRatio();
+
+ const float_4 deltaPhase = freq * args.sampleTime / oversamplingRatio;
+
+ // process sync
+ float_4 sync = syncTrigger[c / 4].process(inputs[SYNC_INPUT].getPolyVoltageSimd(c));
+ phase[c / 4] = simd::ifelse(sync, 0.5f, phase[c / 4]);
+
+
+ for (int i = 0; i < oversamplingRatio; i++) {
+
+ phase[c / 4] += deltaPhase;
+ phase[c / 4] -= simd::floor(phase[c / 4]);
+
+ float_4 sum = {};
+ for (int oct = 0; oct <= highestOutput; oct++) {
+ // derive phases for higher octaves from base phase (this keeps things in sync!)
+ const float_4 n = (float)(1 << oct);
+ // this is on [0, 1]
+ const float_4 effectivePhase = n * simd::fmod(phase[c / 4], 1 / n);
+ const float_4 gainCV = simd::clamp(inputs[GAIN_01F_INPUT + oct].getNormalPolyVoltageSimd(10.f, c) / 10.f, 0.f, 1.0f);
+ const float_4 gain = params[GAIN_01F_PARAM + oct].getValue() * gainCV;
+
+ const float_4 waveTri = 1.0 - 2.0 * simd::abs(2.f * effectivePhase - 1.0);
+ // build square from triangle + comparator
+ const float_4 waveSquare = simd::ifelse(waveTri > pwm, +1.f, -1.f);
+
+ sum += (useTriangleCore ? waveTri : waveSquare) * gain;
+ sum = clamp(sum, -1.f, 1.f);
+
+ if (outputs[OUT_01F_OUTPUT + oct].isConnected()) {
+ oversampler[oct][c/4].getOSBuffer()[i] = sum;
+ sum = 0.f;
+
+ // DEBUG("here %f %f %f %f %f", phase[c/4][0], waveTri[0], sum[0], gain[0], gainCV[0]);
+ }
+
+
+
+ }
+
+ } // end of oversampling loop
+
+ // only downsample required channels
+ for (int oct = 0; oct <= highestOutput; oct++) {
+ if (outputs[OUT_01F_OUTPUT + oct].isConnected()) {
+
+ // downsample (if required)
+ float_4 out = (oversamplingRatio > 1) ? oversampler[oct][c/4].downsample() : oversampler[oct][c/4].getOSBuffer()[0];
+ if (removePulseDC) {
+ out = blockDCFilter[oct][c/4].process(out);
+ }
+
+ outputs[OUT_01F_OUTPUT + oct].setVoltageSimd(5.f * out, c);
+ }
+ }
+ } // end of polyphony loop
+
+ for (int connectedOutput : connectedOutputs) {
+ outputs[OUT_01F_OUTPUT + connectedOutput].setChannels(numActivePolyphonyEngines);
+ }
+ }
+
+ // polyphony is defined by the largest number of active channels on voct, pwm or gain inputs
+ int getNumActivePolyphonyEngines() {
+ int activePolyphonyEngines = 1;
+ for (int c = 0; c < NUM_OUTPUTS; c++) {
+ if (inputs[GAIN_01F_INPUT + c].isConnected()) {
+ activePolyphonyEngines = std::max(activePolyphonyEngines, inputs[GAIN_01F_INPUT + c].getChannels());
+ }
+ }
+ activePolyphonyEngines = std::max({activePolyphonyEngines, inputs[VOCT1_INPUT].getChannels(), inputs[VOCT2_INPUT].getChannels()});
+ activePolyphonyEngines = std::max(activePolyphonyEngines, inputs[PWM_INPUT].getChannels());
+
+ return activePolyphonyEngines;
+ }
+
+ std::vector getConnectedOutputs() {
+ std::vector connectedOutputs;
+ for (int c = 0; c < NUM_OUTPUTS; c++) {
+ if (outputs[OUT_01F_OUTPUT + c].isConnected()) {
+ connectedOutputs.push_back(c);
+ }
+ }
+ return connectedOutputs;
+ }
+
+ json_t* dataToJson() override {
+ json_t* rootJ = json_object();
+ json_object_set_new(rootJ, "removePulseDC", json_boolean(removePulseDC));
+ json_object_set_new(rootJ, "limitPW", json_boolean(limitPW));
+ json_object_set_new(rootJ, "oversamplingIndex", json_integer(oversampler[0][0].getOversamplingIndex()));
+ json_object_set_new(rootJ, "useTriangleCore", json_boolean(useTriangleCore));
+
+ return rootJ;
+ }
+
+ void dataFromJson(json_t* rootJ) override {
+
+ json_t* removePulseDCJ = json_object_get(rootJ, "removePulseDC");
+ if (removePulseDCJ) {
+ 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);
+ onSampleRateChange();
+ }
+
+ json_t* useTriangleCoreJ = json_object_get(rootJ, "useTriangleCore");
+ if (useTriangleCoreJ) {
+ useTriangleCore = json_boolean_value(useTriangleCoreJ);
+ }
+ }
+};
+
+struct OctavesWidget : ModuleWidget {
+ OctavesWidget(Octaves* module) {
+ setModule(module);
+ setPanel(createPanel(asset::plugin(pluginInstance, "res/panels/Octaves.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(52.138, 15.037)), module, Octaves::PWM_CV_PARAM));
+ addParam(createParam(mm2px(Vec(22.171, 30.214)), module, Octaves::OCTAVE_PARAM));
+ addParam(createParamCentered(mm2px(Vec(10.264, 33.007)), module, Octaves::TUNE_PARAM));
+ addParam(createParamCentered(mm2px(Vec(45.384, 40.528)), module, Octaves::PWM_PARAM));
+ addParam(createParam(mm2px(Vec(6.023, 48.937)), module, Octaves::RANGE_PARAM));
+ addParam(createParam(mm2px(Vec(2.9830, 60.342)), module, Octaves::GAIN_01F_PARAM));
+ addParam(createParam(mm2px(Vec(12.967, 60.342)), module, Octaves::GAIN_02F_PARAM));
+ addParam(createParam(mm2px(Vec(22.951, 60.342)), module, Octaves::GAIN_04F_PARAM));
+ addParam(createParam(mm2px(Vec(32.936, 60.342)), module, Octaves::GAIN_08F_PARAM));
+ addParam(createParam(mm2px(Vec(42.920, 60.342)), module, Octaves::GAIN_16F_PARAM));
+ addParam(createParam(mm2px(Vec(52.905, 60.342)), module, Octaves::GAIN_32F_PARAM));
+
+ addInput(createInputCentered(mm2px(Vec(5.247, 15.181)), module, Octaves::VOCT1_INPUT));
+ addInput(createInputCentered(mm2px(Vec(15.282, 15.181)), module, Octaves::VOCT2_INPUT));
+ addInput(createInputCentered(mm2px(Vec(25.316, 15.181)), module, Octaves::SYNC_INPUT));
+ addInput(createInputCentered(mm2px(Vec(37.092, 15.135)), module, Octaves::PWM_INPUT));
+ addInput(createInputCentered(mm2px(Vec(5.247, 100.492)), module, Octaves::GAIN_01F_INPUT));
+ addInput(createInputCentered(mm2px(Vec(15.282, 100.492)), module, Octaves::GAIN_02F_INPUT));
+ addInput(createInputCentered(mm2px(Vec(25.316, 100.492)), module, Octaves::GAIN_04F_INPUT));
+ addInput(createInputCentered(mm2px(Vec(35.35, 100.492)), module, Octaves::GAIN_08F_INPUT));
+ addInput(createInputCentered(mm2px(Vec(45.384, 100.492)), module, Octaves::GAIN_16F_INPUT));
+ addInput(createInputCentered(mm2px(Vec(55.418, 100.492)), module, Octaves::GAIN_32F_INPUT));
+
+ addOutput(createOutputCentered(mm2px(Vec(5.247, 113.508)), module, Octaves::OUT_01F_OUTPUT));
+ addOutput(createOutputCentered(mm2px(Vec(15.282, 113.508)), module, Octaves::OUT_02F_OUTPUT));
+ addOutput(createOutputCentered(mm2px(Vec(25.316, 113.508)), module, Octaves::OUT_04F_OUTPUT));
+ addOutput(createOutputCentered(mm2px(Vec(35.35, 113.508)), module, Octaves::OUT_08F_OUTPUT));
+ addOutput(createOutputCentered(mm2px(Vec(45.384, 113.508)), module, Octaves::OUT_16F_OUTPUT));
+ addOutput(createOutputCentered(mm2px(Vec(55.418, 113.508)), module, Octaves::OUT_32F_OUTPUT));
+
+ }
+
+ void appendContextMenu(Menu* menu) override {
+ Octaves* module = dynamic_cast(this->module);
+ assert(module);
+
+ menu->addChild(new MenuSeparator());
+ menu->addChild(createSubmenuItem("Hardware compatibility", "",
+ [ = ](Menu * menu) {
+ menu->addChild(createBoolPtrMenuItem("Limit pulsewidth (5\%-95\%)", "", &module->limitPW));
+ menu->addChild(createBoolPtrMenuItem("Remove pulse DC", "", &module->removePulseDC));
+ menu->addChild(createBoolPtrMenuItem("Use triangle core", "", &module->useTriangleCore));
+ }
+ ));
+
+ menu->addChild(createIndexSubmenuItem("Oversampling",
+ {"Off", "x2", "x4", "x8"},
+ [ = ]() {
+ return module->oversamplingIndex;
+ },
+ [ = ](int mode) {
+ module->oversamplingIndex = mode;
+ module->onSampleRateChange();
+ }
+ ));
+
+ }
+};
+
+Model* modelOctaves = createModel("Octaves");
\ No newline at end of file
diff --git a/src/Voltio.cpp b/src/Voltio.cpp
index b25df3b..550431c 100644
--- a/src/Voltio.cpp
+++ b/src/Voltio.cpp
@@ -40,7 +40,7 @@ struct Voltio : Module {
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"});
+ configSwitch(RANGE_PARAM, 0.f, 1.f, 0.f, "Range", {"0 to 10", "-5 to +5"});
auto semitonesParam = configParam(SEMITONES_PARAM, 0.f, 11.f, 0.f, "Semitones");
semitonesParam->snapEnabled = true;
diff --git a/src/plugin.cpp b/src/plugin.cpp
index 475f31e..704debd 100644
--- a/src/plugin.cpp
+++ b/src/plugin.cpp
@@ -29,4 +29,5 @@ void init(rack::Plugin *p) {
p->addModel(modelMotionMTR);
p->addModel(modelBurst);
p->addModel(modelVoltio);
+ p->addModel(modelOctaves);
}
diff --git a/src/plugin.hpp b/src/plugin.hpp
index f9d86e8..17addd5 100644
--- a/src/plugin.hpp
+++ b/src/plugin.hpp
@@ -30,6 +30,7 @@ extern Model* modelPonyVCO;
extern Model* modelMotionMTR;
extern Model* modelBurst;
extern Model* modelVoltio;
+extern Model* modelOctaves;
struct Knurlie : SvgScrew {
Knurlie() {
@@ -312,7 +313,7 @@ private:
};
// Creates a Butterworth 2*Nth order highpass filter for blocking DC
-template
+template
struct DCBlockerT {
DCBlockerT() {
@@ -325,7 +326,7 @@ struct DCBlockerT {
recalculateCoefficients();
}
- float process(float x) {
+ T process(T x) {
for (int idx = 0; idx < N; idx++) {
x = blockDCFilter[idx].process(x);
}
@@ -342,17 +343,17 @@ private:
for (int idx = 0; idx < N; idx++) {
float Q = 1.0f / (2.0f * std::cos(firstAngle + idx * poleInc));
- blockDCFilter[idx].setParameters(dsp::BiquadFilter::HIGHPASS, fc_, Q, 1.0f);
+ blockDCFilter[idx].setParameters(dsp::TBiquadFilter::HIGHPASS, fc_, Q, 1.0f);
}
}
float fc_;
static const int order = 2 * N;
- dsp::BiquadFilter blockDCFilter[N];
+ dsp::TBiquadFilter blockDCFilter[N];
};
-typedef DCBlockerT<2> DCBlocker;
+typedef DCBlockerT<2, float> DCBlocker;
/** When triggered, holds a high value for a specified time before going low again */
struct PulseGenerator_4 {
From aa72c130d0e2db44b98cbc65bf65d4e74438fda9 Mon Sep 17 00:00:00 2001
From: Ewan <915048+hemmer@users.noreply.github.com>
Date: Thu, 4 Apr 2024 20:50:57 +0100
Subject: [PATCH 3/4] Update plugin.json
---
plugin.json | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/plugin.json b/plugin.json
index 3befb93..80f7c48 100644
--- a/plugin.json
+++ b/plugin.json
@@ -316,8 +316,8 @@
"modularGridUrl": "https://www.modulargrid.net/e/befaco-octaves-vco",
"tags": [
"Hardware clone",
- "VCO"
+ "Oscillator"
]
}
]
-}
\ No newline at end of file
+}
From b6cb9e858709e5ea31236b259ff5e81be8800563 Mon Sep 17 00:00:00 2001
From: Ewan <915048+hemmer@users.noreply.github.com>
Date: Thu, 18 Apr 2024 19:42:01 +0100
Subject: [PATCH 4/4] MidiThing v2 bridge (#48)
Better default resampling for Octaves
---
CHANGELOG.md | 7 +
docs/MIDIThingV2.md | 28 +
docs/img/MidiThingV2.png | Bin 0 -> 104621 bytes
docs/img/UpdateRate.png | Bin 0 -> 104056 bytes
docs/img/UpdateRatesScope.png | Bin 0 -> 772282 bytes
docs/img/VoltageRange.png | Bin 0 -> 30416 bytes
plugin.json | 17 +-
res/panels/MidiThing.svg | 5595 +++++++++++++++++++++++++++++++++
src/MidiThing.cpp | 804 +++++
src/Octaves.cpp | 71 +-
src/Voltio.cpp | 17 +-
src/plugin.cpp | 1 +
src/plugin.hpp | 1 +
13 files changed, 6467 insertions(+), 74 deletions(-)
create mode 100644 docs/MIDIThingV2.md
create mode 100644 docs/img/MidiThingV2.png
create mode 100644 docs/img/UpdateRate.png
create mode 100644 docs/img/UpdateRatesScope.png
create mode 100644 docs/img/VoltageRange.png
create mode 100644 res/panels/MidiThing.svg
create mode 100644 src/MidiThing.cpp
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 58da59d..0142fca 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,6 +1,13 @@
# Change Log
+## v2.7.0
+ * Midi Thing 2
+ * Initial release
+ * Octaves
+ * Better default oversampling setting (x4)
+
+
## v2.6.0
* Octaves
* Initial release
diff --git a/docs/MIDIThingV2.md b/docs/MIDIThingV2.md
new file mode 100644
index 0000000..b61132e
--- /dev/null
+++ b/docs/MIDIThingV2.md
@@ -0,0 +1,28 @@
+# MIDI Thing v2
+
+The original MIDI Thing v2 hardware unit is described as follows:
+
+> Midi Thing v2 is a flexible MIDI to CV converter. Allowing polyphonic notes handling, envelope and LFO generation as well as all available MIDI messages to be converted into CV. This is a huge upgrade from our previous beloved MIDI Thing, which adds a screen for easy configuration,12 assignable ports, TRS, USB Host and Device, MIDI merge OUT, a web configuration tool, and a VCV rack Bridge counterpart.
+
+The VCV counterpart is designed to allow users to quickly get up and running with their hardware, i.e. sending CV from VCV to the hardware unit.
+
+## Setup
+
+To use, first ensure the MIDI Thing v2 is plugged into your computer, and visible as a MIDI device. Then select it, either from the top of the module, or the right click context menu. Then click "SYNC" - this puts the MIDI Thing into a preset designed to work with VCV Rack, and syncronises settings/voltage ranges etc. Note that for now, sync is one-way (VCV to hardware).
+
+
+
+## Usage
+
+To use, simply wire CV which you wish to send to the hardware to the matching input on the VCV module. Note that you will need to select the range, which can be done by right-clicking on the matching box (see below). Options are 0/10v, -5/5v, -10/0v, 0/8v, 0/5v. Note that the module is **not** designed to work with audio rate signals, just CV.
+
+
+
+## Update Rate
+
+Midi Thing v2 VCV allows the user to configure the update rate at which data is sent over MIDI. This must be shared between the channels, so if we set the hardware to update at 1 kHz, 1 active channel will update at 1 kHz, 2 active channels will update at 500 Hz, 4 active channels at 250 Hz and so on. The total update rate (to be shared between channels) is set from the context menu, noting that higher update rates will use more CPU. The effect of the update rate on a 90 Hz saw (blue trace) can be seen in the bottom image, specifically that the temporal resolution of the reconstructed signal (red traces) improves as the update rate is increased from 500 Hz to 1000 Hz to 2000 Hz.
+
+
+
+
+
diff --git a/docs/img/MidiThingV2.png b/docs/img/MidiThingV2.png
new file mode 100644
index 0000000000000000000000000000000000000000..6d3dc95f1b18df9f4022b15ba3f569af207dfce9
GIT binary patch
literal 104621
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