Browse Source

First attempt at Bypass

Fix #53

* EvenVCO complete re-write, added hard sync
* Avoid allocations on audio thread for Noise Plethora / Octaves
pull/55/head
hemmer 1 year ago
parent
commit
1126c56838
12 changed files with 1310 additions and 180 deletions
  1. +12
    -0
      CHANGELOG.md
  2. +15
    -3
      plugin.json
  3. +866
    -0
      res/panels/Bypass.svg
  4. +174
    -0
      src/Bypass.cpp
  5. +186
    -135
      src/EvenVCO.cpp
  6. +33
    -28
      src/NoisePlethora.cpp
  7. +11
    -11
      src/Octaves.cpp
  8. +1
    -1
      src/noise-plethora/plugins/Banks.cpp
  9. +2
    -1
      src/noise-plethora/plugins/Banks.hpp
  10. +1
    -1
      src/noise-plethora/plugins/ProgramSelector.hpp
  11. +1
    -0
      src/plugin.cpp
  12. +8
    -0
      src/plugin.hpp

+ 12
- 0
CHANGELOG.md View File

@@ -1,5 +1,17 @@
# Change Log

## v2.8.0
* Molten Bypass
* Initial release
* EvenVCO
* Complete re-write for better FM performance
* Hard sync added
* Octaves
* Avoid allocation in the audio thread (thanks @danngreen)
* Noise Plethora
* Fix labels
* Avoid std::string allocations on audio thread (thanks @danngreen)
## v2.7.1
* Midi Thing 2
* Remove -10 to 0 V configuration


+ 15
- 3
plugin.json View File

@@ -1,6 +1,6 @@
{
"slug": "Befaco",
"version": "2.7.1",
"version": "2.8.0",
"license": "GPL-3.0-or-later",
"name": "Befaco",
"brand": "Befaco",
@@ -314,7 +314,7 @@
"description": "An accurate voltage source and precision adder.",
"manualUrl": "https://www.befaco.org/voltio/",
"modularGridUrl": "https://www.modulargrid.net/e/befaco-voltio",
"tags": [
"tags": [
"Hardware clone",
"Polyphonic",
"Utility"
@@ -331,6 +331,18 @@
"Oscillator",
"Polyphonic"
]
},
{
"slug": "Bypass",
"name": "Bypass",
"description": "A Stereo bypass module to gate control the send of your signals to your favorite effect!",
"manualUrl": "https://www.befaco.org/molten-bypass/",
"modularGridUrl": "https://www.modulargrid.net/e/befaco-molten-bypass",
"tags": [
"Hardware clone",
"Mixer",
"Utility"
]
}
]
}
}

+ 866
- 0
res/panels/Bypass.svg
File diff suppressed because it is too large
View File


+ 174
- 0
src/Bypass.cpp View File

@@ -0,0 +1,174 @@
#include "plugin.hpp"

using namespace simd;

struct Bypass : Module {
enum ParamId {
MODE_PARAM,
FX_GAIN_PARAM,
LAUNCH_MODE_PARAM,
LAUNCH_BUTTON_PARAM,
PARAMS_LEN
};
enum InputId {
IN_R_INPUT,
FROM_FX_L_INPUT,
FROM_FX_R_INPUT,
LAUNCH_INPUT,
IN_L_INPUT,
INPUTS_LEN
};
enum OutputId {
TOFX_L_OUTPUT,
TOFX_R_OUTPUT,
OUT_L_OUTPUT,
OUT_R_OUTPUT,
OUTPUTS_LEN
};
enum LightId {
LAUNCH_LED,
LIGHTS_LEN
};
enum LatchMode {
TOGGLE_MODE, // i.e. latch
MOMENTARY_MODE // i.e. gate
};
enum ReturnMode {
HARD_MODE,
SOFT_MODE
};
LatchMode latchMode = LatchMode::MOMENTARY_MODE;
ReturnMode returnMode = ReturnMode::HARD_MODE;
ParamQuantity* launchParam;
dsp::SchmittTrigger launchCvTrigger;
dsp::BooleanTrigger launchButtonTrigger;
dsp::BooleanTrigger latchTrigger;
dsp::SlewLimiter clickFilter;

Bypass() {
config(PARAMS_LEN, INPUTS_LEN, OUTPUTS_LEN, LIGHTS_LEN);
configSwitch(MODE_PARAM, 0.f, 1.f, 0.f, "Return mode", {"Hard", "Soft"});
configParam(FX_GAIN_PARAM, -30.f, 30.f, 0.f, "FX Gain");
configSwitch(LAUNCH_MODE_PARAM, 0.f, 1.f, 0.f, "Launch Mode", {"Latch (Toggle)", "Gate (Momentary)"});
launchParam = configButton(LAUNCH_BUTTON_PARAM, "Launch");

configInput(IN_L_INPUT, "Left");
configInput(IN_R_INPUT, "Right");
configInput(FROM_FX_L_INPUT, "From FX L");
configInput(FROM_FX_R_INPUT, "From FX R");
configInput(LAUNCH_INPUT, "Launch");

configOutput(TOFX_L_OUTPUT, "To FX L");
configOutput(TOFX_R_OUTPUT, "To FX R");
configOutput(OUT_L_OUTPUT, "Left");
configOutput(OUT_R_OUTPUT, "Right");

configBypass(IN_L_INPUT, OUT_L_OUTPUT);
configBypass(IN_R_INPUT, OUT_R_OUTPUT);

clickFilter.rise = 1 / 0.01f; // 0.01 ms
clickFilter.fall = 1 / 0.01f; // 0.01 ms
}

bool active = false;
void process(const ProcessArgs& args) override {

const int maxChannels = std::max(inputs[IN_L_INPUT].getChannels(), inputs[IN_R_INPUT].getChannels());

latchMode = (LatchMode) params[LAUNCH_MODE_PARAM].getValue();
const ReturnMode returnMode = (ReturnMode) params[MODE_PARAM].getValue();


const bool launchCvTriggered = launchCvTrigger.process(inputs[LAUNCH_INPUT].getVoltage());
const bool launchButtonPressed = launchButtonTrigger.process(params[LAUNCH_BUTTON_PARAM].getValue());

// logical or (high if either high)
const float launchValue = std::max(launchCvTrigger.isHigh(), launchButtonTrigger.isHigh());
if (latchMode == LatchMode::TOGGLE_MODE) {
const bool risingEdge = launchCvTriggered || launchButtonPressed;

// TODO: sometimes misses?
if (risingEdge) {
active = !active;
}
}

const float fxGain = std::pow(10, params[FX_GAIN_PARAM].getValue() / 20.0f);
const float sendActive = clickFilter.process(args.sampleTime, (latchMode == LatchMode::TOGGLE_MODE) ? active : launchValue);

for (int c = 0; c < maxChannels; c += 4) {

const float_4 inL = inputs[IN_L_INPUT].getPolyVoltageSimd<float_4>(c);
const float_4 inR = inputs[IN_R_INPUT].getNormalPolyVoltageSimd<float_4>(inL, c);

outputs[TOFX_L_OUTPUT].setVoltageSimd<float_4>(inL * fxGain * sendActive, c);
outputs[TOFX_R_OUTPUT].setVoltageSimd<float_4>(inR * fxGain * sendActive, c);

const float_4 fxLeftReturn = inputs[FROM_FX_L_INPUT].getPolyVoltageSimd<float_4>(c);
const float_4 fxRightReturn = inputs[FROM_FX_R_INPUT].getPolyVoltageSimd<float_4>(c);

if (returnMode == ReturnMode::HARD_MODE) {
outputs[OUT_L_OUTPUT].setVoltageSimd<float_4>(inL * (1 - sendActive) + sendActive * fxLeftReturn, c);
outputs[OUT_R_OUTPUT].setVoltageSimd<float_4>(inR * (1 - sendActive) + sendActive * fxRightReturn, c);
}
else {
outputs[OUT_L_OUTPUT].setVoltageSimd<float_4>(inL * (1 - sendActive) + fxLeftReturn, c);
outputs[OUT_R_OUTPUT].setVoltageSimd<float_4>(inR * (1 - sendActive) + fxRightReturn, c);
}
}

outputs[OUT_R_OUTPUT].setVoltage(sendActive);

lights[LAUNCH_LED].setBrightness(sendActive);
}
};


using BefacoRedLightButton = LightButton<BefacoButton, VeryLargeSimpleLight<TRedLight<app::ModuleLightWidget>>>;


struct BypassWidget : ModuleWidget {

SvgSwitch* launchParam;

BypassWidget(Bypass* module) {
setModule(module);
setPanel(createPanel(asset::plugin(pluginInstance, "res/panels/Bypass.svg")));

addChild(createWidget<Knurlie>(Vec(RACK_GRID_WIDTH, 0)));
addChild(createWidget<Knurlie>(Vec(RACK_GRID_WIDTH, RACK_GRID_HEIGHT - RACK_GRID_WIDTH)));

addParam(createParam<CKSSHoriz2>(mm2px(Vec(6.7, 63.263)), module, Bypass::MODE_PARAM));
addParam(createParamCentered<BefacoTinyKnobWhite>(mm2px(Vec(10.0, 78.903)), module, Bypass::FX_GAIN_PARAM));
addParam(createParam<CKSSNarrow>(mm2px(Vec(13.8, 91.6)), module, Bypass::LAUNCH_MODE_PARAM));

launchParam = createLightParamCentered<BefacoRedLightButton>(mm2px(Vec(10.0, 111.287)), module, Bypass::LAUNCH_BUTTON_PARAM, Bypass::LAUNCH_LED);
addParam(launchParam);

addInput(createInputCentered<BefacoInputPort>(mm2px(Vec(15.016, 15.03)), module, Bypass::IN_R_INPUT));
addInput(createInputCentered<BefacoInputPort>(mm2px(Vec(4.947, 40.893)), module, Bypass::FROM_FX_L_INPUT));
addInput(createInputCentered<BefacoInputPort>(mm2px(Vec(15.001, 40.893)), module, Bypass::FROM_FX_R_INPUT));
addInput(createInputCentered<BefacoInputPort>(mm2px(Vec(6.648, 95.028)), module, Bypass::LAUNCH_INPUT));
addInput(createInputCentered<BefacoInputPort>(mm2px(Vec(4.947, 15.03)), module, Bypass::IN_L_INPUT));

addOutput(createOutputCentered<BefacoOutputPort>(mm2px(Vec(4.957, 27.961)), module, Bypass::TOFX_L_OUTPUT));
addOutput(createOutputCentered<BefacoOutputPort>(mm2px(Vec(14.957, 27.961)), module, Bypass::TOFX_R_OUTPUT));
addOutput(createOutputCentered<BefacoOutputPort>(mm2px(Vec(4.947, 53.846)), module, Bypass::OUT_L_OUTPUT));
addOutput(createOutputCentered<BefacoOutputPort>(mm2px(Vec(14.957, 53.824)), module, Bypass::OUT_R_OUTPUT));
}

void draw(const DrawArgs& args) override {

Bypass* module = dynamic_cast<Bypass*>(this->module);

if (module != nullptr) {
launchParam->momentary = module->latchMode == Bypass::LatchMode::MOMENTARY_MODE;
launchParam->latch = !launchParam->momentary;
}

ModuleWidget::draw(args);
}
};


Model* modelBypass = createModel<Bypass, BypassWidget>("Bypass");

+ 186
- 135
src/EvenVCO.cpp View File

@@ -1,4 +1,5 @@
#include "plugin.hpp"
#include "ChowDSP.hpp"

using simd::float_4;

@@ -26,20 +27,11 @@ struct EvenVCO : Module {
NUM_OUTPUTS
};

float_4 phase[4] = {};
float_4 tri[4] = {};

/** The value of the last sync input */
float sync = 0.0;
/** The outputs */
/** Whether we are past the pulse width already */
bool halfPhase[PORT_MAX_CHANNELS] = {};
float_4 phase[4] = {};
dsp::TSchmittTrigger<float_4> syncTrigger[4];
bool removePulseDC = true;

dsp::MinBlepGenerator<16, 32> triSquareMinBlep[PORT_MAX_CHANNELS];
dsp::MinBlepGenerator<16, 32> doubleSawMinBlep[PORT_MAX_CHANNELS];
dsp::MinBlepGenerator<16, 32> sawMinBlep[PORT_MAX_CHANNELS];
dsp::MinBlepGenerator<16, 32> squareMinBlep[PORT_MAX_CHANNELS];
bool limitPW = true;

EvenVCO() {
config(NUM_PARAMS, NUM_INPUTS, NUM_OUTPUTS);
@@ -51,7 +43,7 @@ struct EvenVCO : Module {
configInput(PITCH1_INPUT, "Pitch 1");
configInput(PITCH2_INPUT, "Pitch 2");
configInput(FM_INPUT, "FM");
configInput(SYNC_INPUT, "Sync (not implemented)");
configInput(SYNC_INPUT, "Sync");
configInput(PWM_INPUT, "Pulse Width Modulation");

configOutput(TRI_OUTPUT, "Triangle");
@@ -59,157 +51,191 @@ struct EvenVCO : Module {
configOutput(EVEN_OUTPUT, "Even");
configOutput(SAW_OUTPUT, "Sawtooth");
configOutput(SQUARE_OUTPUT, "Square");

// calculate up/downsampling rates
onSampleRateChange();
}

void process(const ProcessArgs& args) override {
void onSampleRateChange() override {
float sampleRate = APP->engine->getSampleRate();
for (int i = 0; i < NUM_OUTPUTS; ++i) {
for (int c = 0; c < 4; c++) {
oversampler[i][c].setOversamplingIndex(oversamplingIndex);
oversampler[i][c].reset(sampleRate);
}
}

int channels_pitch1 = inputs[PITCH1_INPUT].getChannels();
int channels_pitch2 = inputs[PITCH2_INPUT].getChannels();
const float lowFreqRegime = oversampler[0][0].getOversamplingRatio() * 1e-3 * sampleRate;
DEBUG("Low freq regime: %g", lowFreqRegime);
}

int channels = 1;
channels = std::max(channels, channels_pitch1);
channels = std::max(channels, channels_pitch2);
float_4 aliasSuppressedTri(float_4* phases) {
float_4 triBuffer[3];
for (int i = 0; i < 3; ++i) {
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]);
}

float pitch_0 = 1.f + std::round(params[OCTAVE_PARAM].getValue()) + params[TUNE_PARAM].getValue() / 12.f;
float_4 aliasSuppressedSaw(float_4* phases) {
float_4 sawBuffer[3];
for (int i = 0; i < 3; ++i) {
float_4 p = 2 * phases[i] - 1.0; // range -1 to +1
sawBuffer[i] = (p * p * p - p) / 6.0; // eq 11
}

// Compute frequency, pitch is 1V/oct
float_4 pitch[4] = {};
for (int c = 0; c < channels; c += 4)
pitch[c / 4] = pitch_0;
return (sawBuffer[0] - 2.0 * sawBuffer[1] + sawBuffer[2]);
}

if (inputs[PITCH1_INPUT].isConnected()) {
for (int c = 0; c < channels; c += 4)
pitch[c / 4] += inputs[PITCH1_INPUT].getPolyVoltageSimd<float_4>(c);
float_4 aliasSuppressedDoubleSaw(float_4* phases) {
float_4 sawBuffer[3];
for (int i = 0; i < 3; ++i) {
float_4 p = 4.0 * simd::ifelse(phases[i] < 0.5, phases[i], phases[i] - 0.5) - 1.0;
sawBuffer[i] = (p * p * p - p) / 24.0; // eq 11 (modified for doubled freq)
}

if (inputs[PITCH2_INPUT].isConnected()) {
for (int c = 0; c < channels; c += 4)
pitch[c / 4] += inputs[PITCH2_INPUT].getPolyVoltageSimd<float_4>(c);
}
return (sawBuffer[0] - 2.0 * sawBuffer[1] + sawBuffer[2]);
}

if (inputs[FM_INPUT].isConnected()) {
for (int c = 0; c < channels; c += 4)
pitch[c / 4] += inputs[FM_INPUT].getPolyVoltageSimd<float_4>(c) / 4.f;
}
float_4 aliasSuppressedOffsetSaw(float_4* phases, float_4 pw) {
float_4 sawOffsetBuff[3];

float_4 freq[4] = {};
for (int c = 0; c < channels; c += 4) {
freq[c / 4] = dsp::FREQ_C4 * simd::pow(2.f, pitch[c / 4]);
freq[c / 4] = clamp(freq[c / 4], 0.f, 20000.f);
for (int i = 0; i < 3; ++i) {
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]);
}

// Pulse width
float_4 pw[4] = {};
for (int c = 0; c < channels; c += 4)
pw[c / 4] = params[PWM_PARAM].getValue();
chowdsp::VariableOversampling<6, float_4> oversampler[NUM_OUTPUTS][4]; // uses a 2*6=12th order Butterworth filter
int oversamplingIndex = 2; // default is 2^oversamplingIndex == x4 oversampling

if (inputs[PWM_INPUT].isConnected()) {
for (int c = 0; c < channels; c += 4)
pw[c / 4] += inputs[PWM_INPUT].getPolyVoltageSimd<float_4>(c) / 5.f;
}
void process(const ProcessArgs& args) override {

// pitch inputs determine number of polyphony engines
const int channels = std::max({1, inputs[PITCH1_INPUT].getChannels(), inputs[PITCH2_INPUT].getChannels()});

const float pitchKnobs = 1.f + std::round(params[OCTAVE_PARAM].getValue()) + params[TUNE_PARAM].getValue() / 12.f;
const int oversamplingRatio = oversampler[0][0].getOversamplingRatio();

float_4 deltaPhase[4] = {};
float_4 oldPhase[4] = {};
for (int c = 0; c < channels; c += 4) {
pw[c / 4] = rescale(clamp(pw[c / 4], -1.0f, 1.0f), -1.0f, 1.0f, 0.05f, 1.0f - 0.05f);
float_4 pw = simd::clamp(params[PWM_PARAM].getValue() + inputs[PWM_INPUT].getPolyVoltageSimd<float_4>(c) / 5.f, -1.f, 1.f);
if (limitPW) {
pw = simd::rescale(pw, -1, +1, 0.05f, 0.95f);
}
else {
pw = simd::rescale(pw, -1.f, +1.f, 0.f, 1.f);
}

// Advance phase
deltaPhase[c / 4] = clamp(freq[c / 4] * args.sampleTime, 1e-6f, 0.5f);
oldPhase[c / 4] = phase[c / 4];
phase[c / 4] += deltaPhase[c / 4];
}
const float_4 fmVoltage = inputs[FM_INPUT].getPolyVoltageSimd<float_4>(c) * 0.25f;
const float_4 pitch = inputs[PITCH1_INPUT].getPolyVoltageSimd<float_4>(c) + inputs[PITCH2_INPUT].getPolyVoltageSimd<float_4>(c);
const float_4 freq = dsp::FREQ_C4 * simd::pow(2.f, pitchKnobs + pitch + fmVoltage);
const float_4 deltaBasePhase = simd::clamp(freq * args.sampleTime / oversamplingRatio, 1e-6, 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);

// the next block can't be done with SIMD instructions, but should at least be completed with
// blocks of 4 (otherwise popping artfifacts are generated from invalid phase/oldPhase/deltaPhase)
const int channelsRoundedUpNearestFour = (1 + (channels - 1) / 4) * 4;
for (int c = 0; c < channelsRoundedUpNearestFour; c++) {
// 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);

if (oldPhase[c / 4].s[c % 4] < 0.5 && phase[c / 4].s[c % 4] >= 0.5) {
float crossing = -(phase[c / 4].s[c % 4] - 0.5) / deltaPhase[c / 4].s[c % 4];
triSquareMinBlep[c].insertDiscontinuity(crossing, 2.f);
doubleSawMinBlep[c].insertDiscontinuity(crossing, -2.f);
}
// hard sync
const float_4 syncMask = syncTrigger[c / 4].process(inputs[SYNC_INPUT].getPolyVoltageSimd<float_4>(c));
phase[c / 4] = simd::ifelse(syncMask, 0.5f, phase[c / 4]);

if (!halfPhase[c] && phase[c / 4].s[c % 4] >= pw[c / 4].s[c % 4]) {
float crossing = -(phase[c / 4].s[c % 4] - pw[c / 4].s[c % 4]) / deltaPhase[c / 4].s[c % 4];
squareMinBlep[c].insertDiscontinuity(crossing, 2.f);
halfPhase[c] = true;
}
float_4* osBufferTri = oversampler[TRI_OUTPUT][c / 4].getOSBuffer();
float_4* osBufferSaw = oversampler[SAW_OUTPUT][c / 4].getOSBuffer();
float_4* osBufferSin = oversampler[SINE_OUTPUT][c / 4].getOSBuffer();
float_4* osBufferSquare = oversampler[SQUARE_OUTPUT][c / 4].getOSBuffer();
float_4* osBufferEven = oversampler[EVEN_OUTPUT][c / 4].getOSBuffer();
for (int i = 0; i < oversamplingRatio; ++i) {

// Reset phase if at end of cycle
if (phase[c / 4].s[c % 4] >= 1.f) {
phase[c / 4].s[c % 4] -= 1.f;
float crossing = -phase[c / 4].s[c % 4] / deltaPhase[c / 4].s[c % 4];
triSquareMinBlep[c].insertDiscontinuity(crossing, -2.f);
doubleSawMinBlep[c].insertDiscontinuity(crossing, -2.f);
squareMinBlep[c].insertDiscontinuity(crossing, -2.f);
sawMinBlep[c].insertDiscontinuity(crossing, -2.f);
halfPhase[c] = false;
}
}
phase[c / 4] += deltaBasePhase;
// ensure within [0, 1]
phase[c / 4] -= simd::floor(phase[c / 4]);

float_4 triSquareMinBlepOut[4] = {};
float_4 doubleSawMinBlepOut[4] = {};
float_4 sawMinBlepOut[4] = {};
float_4 squareMinBlepOut[4] = {};

float_4 triSquare[4] = {};
float_4 sine[4] = {};
float_4 doubleSaw[4] = {};

float_4 even[4] = {};
float_4 saw[4] = {};
float_4 square[4] = {};
float_4 triOut[4] = {};

for (int c = 0; c < channelsRoundedUpNearestFour; c++) {
triSquareMinBlepOut[c / 4].s[c % 4] = triSquareMinBlep[c].process();
doubleSawMinBlepOut[c / 4].s[c % 4] = doubleSawMinBlep[c].process();
sawMinBlepOut[c / 4].s[c % 4] = sawMinBlep[c].process();
squareMinBlepOut[c / 4].s[c % 4] = squareMinBlep[c].process();
}
float_4 phases[3]; // phase as extrapolated to the current and two previous samples

for (int c = 0; c < channels; c += 4) {
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];

triSquare[c / 4] = simd::ifelse((phase[c / 4] < 0.5f), -1.f, +1.f);
triSquare[c / 4] += triSquareMinBlepOut[c / 4];
if (outputs[SINE_OUTPUT].isConnected() || outputs[EVEN_OUTPUT].isConnected()) {
// sin doesn't need PDW
osBufferSin[i] = -simd::cos(2.0 * M_PI * phase[c / 4]);
}

// Integrate square for triangle
if (outputs[TRI_OUTPUT].isConnected()) {
const float_4 dpwOrder1 = 1.0 - 2.0 * simd::abs(2 * phase[c / 4] - 1.0);
const float_4 dpwOrder3 = aliasSuppressedTri(phases) * denominatorInv;

tri[c / 4] += (4.f * triSquare[c / 4]) * (freq[c / 4] * args.sampleTime);
tri[c / 4] *= (1.f - 40.f * args.sampleTime);
triOut[c / 4] = 5.f * tri[c / 4];
osBufferTri[i] = simd::ifelse(lowFreqRegime, dpwOrder1, dpwOrder3);
}

sine[c / 4] = 5.f * simd::cos(2 * M_PI * phase[c / 4]);
if (outputs[SAW_OUTPUT].isConnected()) {
const float_4 dpwOrder1 = 2 * phase[c / 4] - 1.0;
const float_4 dpwOrder3 = aliasSuppressedSaw(phases) * denominatorInv;

// minBlep adds a small amount of DC that becomes significant at higher frequencies,
// this subtracts DC based on empirical observvations about the scaling relationship
const float sawCorrect = -5.7;
const float_4 sawDCComp = deltaPhase[c / 4] * sawCorrect;
osBufferSaw[i] = simd::ifelse(lowFreqRegime, dpwOrder1, dpwOrder3);
}

doubleSaw[c / 4] = simd::ifelse((phase[c / 4] < 0.5), (-1.f + 4.f * phase[c / 4]), (-1.f + 4.f * (phase[c / 4] - 0.5f)));
doubleSaw[c / 4] += doubleSawMinBlepOut[c / 4];
doubleSaw[c / 4] += 2.f * sawDCComp;
doubleSaw[c / 4] *= 5.f;
if (outputs[SQUARE_OUTPUT].isConnected()) {

even[c / 4] = 0.55 * (doubleSaw[c / 4] + 1.27 * sine[c / 4]);
saw[c / 4] = -1.f + 2.f * phase[c / 4];
saw[c / 4] += sawMinBlepOut[c / 4];
saw[c / 4] += sawDCComp;
saw[c / 4] *= 5.f;
float_4 dpwOrder1 = simd::ifelse(phase[c / 4] < pw, -1.0, +1.0);
dpwOrder1 -= removePulseDC ? 2.f * (0.5f - pw) : 0.f;

square[c / 4] = simd::ifelse((phase[c / 4] < pw[c / 4]), -1.f, +1.f);
square[c / 4] += squareMinBlepOut[c / 4];
square[c / 4] += removePulseDC * 2.f * (pw[c / 4] - 0.5f);
square[c / 4] *= 5.f;
float_4 saw = aliasSuppressedSaw(phases);
float_4 sawOffset = aliasSuppressedOffsetSaw(phases, pw);
float_4 dpwOrder3 = (saw - sawOffset) * denominatorInv + pulseDCOffset;

// Set outputs
outputs[TRI_OUTPUT].setVoltageSimd(triOut[c / 4], c);
outputs[SINE_OUTPUT].setVoltageSimd(sine[c / 4], c);
outputs[EVEN_OUTPUT].setVoltageSimd(even[c / 4], c);
outputs[SAW_OUTPUT].setVoltageSimd(saw[c / 4], c);
outputs[SQUARE_OUTPUT].setVoltageSimd(square[c / 4], c);
}
osBufferSquare[i] = simd::ifelse(lowFreqRegime, dpwOrder1, dpwOrder3);
}

if (outputs[EVEN_OUTPUT].isConnected()) {

float_4 dpwOrder1 = 4.0 * simd::ifelse(phase[c / 4] < 0.5, phase[c / 4], phase[c / 4] - 0.5) - 1.0;
float_4 dpwOrder3 = aliasSuppressedDoubleSaw(phases) * denominatorInv;
float_4 doubleSaw = simd::ifelse(lowFreqRegime, dpwOrder1, dpwOrder3);
osBufferEven[i] = 0.55 * (doubleSaw + 1.27 * osBufferSin[i]);
}


} // end of oversampling loop

// downsample (if required)
if (outputs[SINE_OUTPUT].isConnected()) {
const float_4 outSin = (oversamplingRatio > 1) ? oversampler[SINE_OUTPUT][c / 4].downsample() : osBufferSin[0];
outputs[SINE_OUTPUT].setVoltageSimd(5.f * outSin, c);
}

if (outputs[TRI_OUTPUT].isConnected()) {
const float_4 outTri = (oversamplingRatio > 1) ? oversampler[TRI_OUTPUT][c / 4].downsample() : osBufferTri[0];
outputs[TRI_OUTPUT].setVoltageSimd(5.f * outTri, c);
}

if (outputs[SAW_OUTPUT].isConnected()) {
const float_4 outSaw = (oversamplingRatio > 1) ? oversampler[SAW_OUTPUT][c / 4].downsample() : osBufferSaw[0];
outputs[SAW_OUTPUT].setVoltageSimd(5.f * outSaw, c);
}

if (outputs[SQUARE_OUTPUT].isConnected()) {
const float_4 outSquare = (oversamplingRatio > 1) ? oversampler[SQUARE_OUTPUT][c / 4].downsample() : osBufferSquare[0];
outputs[SQUARE_OUTPUT].setVoltageSimd(5.f * outSquare, c);
}

if (outputs[EVEN_OUTPUT].isConnected()) {
const float_4 outEven = (oversamplingRatio > 1) ? oversampler[EVEN_OUTPUT][c / 4].downsample() : osBufferEven[0];
outputs[EVEN_OUTPUT].setVoltageSimd(5.f * outEven, c);
}

} // end of channels loop

// Outputs
outputs[TRI_OUTPUT].setChannels(channels);
@@ -223,6 +249,8 @@ struct EvenVCO : Module {
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()));
return rootJ;
}

@@ -231,6 +259,17 @@ struct EvenVCO : Module {
if (pulseDCJ) {
removePulseDC = json_boolean_value(pulseDCJ);
}

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();
}
}
};

@@ -269,10 +308,22 @@ struct EvenVCOWidget : ModuleWidget {

menu->addChild(new MenuSeparator());
menu->addChild(createSubmenuItem("Hardware compatibility", "",
[ = ](Menu * menu) {
[ = ](Menu * menu) {
menu->addChild(createBoolPtrMenuItem("Remove DC from pulse", "", &module->removePulseDC));
}
));
menu->addChild(createBoolPtrMenuItem("Limit pulsewidth (5\%-95\%)", "", &module->limitPW));
}
));

menu->addChild(createIndexSubmenuItem("Oversampling",
{"Off", "x2", "x4", "x8"},
[ = ]() {
return module->oversamplingIndex;
},
[ = ](int mode) {
module->oversamplingIndex = mode;
module->onSampleRateChange();
}
));
}
};



+ 33
- 28
src/NoisePlethora.cpp View File

@@ -160,8 +160,10 @@ struct NoisePlethora : Module {

// section A/B
bool bypassFilters = false;
std::shared_ptr<NoisePlethoraPlugin> algorithm[2]; // pointer to actual algorithm
std::string algorithmName[2]; // variable to cache which algorithm is active (after program CV applied)
std::shared_ptr<NoisePlethoraPlugin> algorithm[2]{nullptr, nullptr}; // pointer to actual algorithm
std::string_view algorithmName[2]{"", ""}; // variable to cache which algorithm is active (after program CV applied)
std::map<std::string_view, std::shared_ptr<NoisePlethoraPlugin>> A_algorithms{};
std::map<std::string_view, std::shared_ptr<NoisePlethoraPlugin>> B_algorithms{};

// filters for A/B
StateVariableFilter2ndOrder svfFilter[2];
@@ -195,11 +197,11 @@ struct NoisePlethora : Module {
configParam(Y_A_PARAM, 0.f, 1.f, 0.5f, "YA");
configParam(CUTOFF_CV_A_PARAM, 0.f, 1.f, 0.f, "Cutoff CV A");
configSwitch(FILTER_TYPE_A_PARAM, 0.f, 2.f, 0.f, "Filter type", {"Lowpass", "Bandpass", "Highpass"});
configParam(PROGRAM_PARAM, -INFINITY, +INFINITY, 0.f, "Program/Bank selection");
configParam(PROGRAM_PARAM, 0, 1, 0.f, "Program/Bank selection");
configSwitch(FILTER_TYPE_B_PARAM, 0.f, 2.f, 0.f, "Filter type", {"Lowpass", "Bandpass", "Highpass"});
configParam(CUTOFF_CV_B_PARAM, 0.f, 1.f, 0.f, "Cutoff B");
configParam(CUTOFF_CV_B_PARAM, 0.f, 1.f, 0.f, "Cutoff CV B");
configParam(X_B_PARAM, 0.f, 1.f, 0.5f, "XB");
configParam(CUTOFF_B_PARAM, 0.f, 1.f, 1.f, "Cutoff CV B");
configParam(CUTOFF_B_PARAM, 0.f, 1.f, 1.f, "Cutoff B");
configParam(RES_B_PARAM, 0.f, 1.f, 0.f, "Resonance B");
configParam(Y_B_PARAM, 0.f, 1.f, 0.5f, "YB");
configSwitch(FILTER_TYPE_C_PARAM, 0.f, 2.f, 0.f, "Filter type", {"Lowpass", "Bandpass", "Highpass"});
@@ -231,6 +233,11 @@ struct NoisePlethora : Module {
getInputInfo(PROG_A_INPUT)->description = "CV sums with active program (0.5V increments)";
getInputInfo(PROG_B_INPUT)->description = "CV sums with active program (0.5V increments)";

for (auto const &entry : MyFactory::Instance()->factoryFunctionRegistry) {
A_algorithms[entry.first] = MyFactory::Instance()->Create(entry.first);
B_algorithms[entry.first] = MyFactory::Instance()->Create(entry.first);
}

setAlgorithm(SECTION_B, "radioOhNo");
setAlgorithm(SECTION_A, "radioOhNo");
onSampleRateChange();
@@ -298,19 +305,19 @@ struct NoisePlethora : Module {
programSelectorWithCV.getSection(SECTION).setBank(bank);
programSelectorWithCV.getSection(SECTION).setProgram(programWithCV);

const std::string newAlgorithmName = programSelectorWithCV.getSection(SECTION).getCurrentProgramName();
std::string_view newAlgorithmName = programSelectorWithCV.getSection(SECTION).getCurrentProgramName();

// this is just a caching check to avoid constantly re-initialisating the algorithms
if (newAlgorithmName != algorithmName[SECTION]) {

algorithm[SECTION] = MyFactory::Instance()->Create(newAlgorithmName);
algorithm[SECTION] = SECTION == Section::SECTION_A ? A_algorithms[newAlgorithmName] : B_algorithms[newAlgorithmName];
algorithmName[SECTION] = newAlgorithmName;

if (algorithm[SECTION]) {
algorithm[SECTION]->init();
}
else {
DEBUG("WARNING: Failed to initialise %s in programSelector", newAlgorithmName.c_str());
DEBUG("WARNING: Failed to initialise %s in programSelector", newAlgorithmName.data());
}
}
}
@@ -433,25 +440,23 @@ struct NoisePlethora : Module {
void processProgramBankKnobLogic(const ProcessArgs& args) {

// program knob will either change program for current bank...
if (programButtonDragged) {
// work out the change (in discrete increments) since the program/bank knob started being dragged
const int delta = (int)(dialResolution * (params[PROGRAM_PARAM].getValue() - programKnobReferenceState));
{

if (programKnobMode == PROGRAM_MODE) {
const int numProgramsForCurrentBank = getBankForIndex(programSelector.getCurrent().getBank()).getSize();
const int currentProgram = programSelector.getCurrent().getProgram();
const int newProgramFromKnob = (int) std::round((numProgramsForCurrentBank - 1) * params[PROGRAM_PARAM].getValue());

if (delta != 0) {
const int newProgramFromKnob = unsigned_modulo(programSelector.getCurrent().getProgram() + delta, numProgramsForCurrentBank);
programKnobReferenceState = params[PROGRAM_PARAM].getValue();
if (newProgramFromKnob != currentProgram) {
setAlgorithmViaProgram(newProgramFromKnob);
}
}
// ...or change bank, (trying to) keep program the same
else {
const int currentBank = programSelector.getCurrent().getBank();
const int newBankFromKnob = (int) std::round((numBanks - 1) * params[PROGRAM_PARAM].getValue());

if (delta != 0) {
const int newBankFromKnob = unsigned_modulo(programSelector.getCurrent().getBank() + delta, numBanks);
programKnobReferenceState = params[PROGRAM_PARAM].getValue();
if (currentBank != newBankFromKnob) {
setAlgorithmViaBank(newBankFromKnob);
}
}
@@ -502,7 +507,7 @@ struct NoisePlethora : Module {
void setAlgorithmViaProgram(int newProgram) {

const int currentBank = programSelector.getCurrent().getBank();
const std::string algorithmName = getBankForIndex(currentBank).getProgramName(newProgram);
std::string_view algorithmName = getBankForIndex(currentBank).getProgramName(newProgram);
const int section = programSelector.getMode();

setAlgorithm(section, algorithmName);
@@ -513,13 +518,13 @@ struct NoisePlethora : Module {
const int currentProgram = programSelector.getCurrent().getProgram();
// the new bank may not have as many algorithms
const int currentProgramInNewBank = clamp(currentProgram, 0, getBankForIndex(newBank).getSize() - 1);
const std::string algorithmName = getBankForIndex(newBank).getProgramName(currentProgramInNewBank);
const std::string_view algorithmName = getBankForIndex(newBank).getProgramName(currentProgramInNewBank);
const int section = programSelector.getMode();

setAlgorithm(section, algorithmName);
}

void setAlgorithm(int section, std::string algorithmName) {
void setAlgorithm(int section, std::string_view algorithmName) {

if (section > 1) {
return;
@@ -537,7 +542,7 @@ struct NoisePlethora : Module {
}
}

DEBUG("WARNING: Didn't find %s in programSelector", algorithmName.c_str());
DEBUG("WARNING: Didn't find %s in programSelector", algorithmName.data());
}

void dataFromJson(json_t* rootJ) override {
@@ -565,8 +570,8 @@ struct NoisePlethora : Module {
json_t* dataToJson() override {
json_t* rootJ = json_object();

json_object_set_new(rootJ, "algorithmA", json_string(programSelector.getA().getCurrentProgramName().c_str()));
json_object_set_new(rootJ, "algorithmB", json_string(programSelector.getB().getCurrentProgramName().c_str()));
json_object_set_new(rootJ, "algorithmA", json_string(programSelector.getA().getCurrentProgramName().data()));
json_object_set_new(rootJ, "algorithmB", json_string(programSelector.getB().getCurrentProgramName().data()));

json_object_set_new(rootJ, "bypassFilters", json_boolean(bypassFilters));
json_object_set_new(rootJ, "blockDC", json_boolean(blockDC));
@@ -648,7 +653,7 @@ struct NoisePlethoraLEDDisplay : LightWidget {
}

void setTooltip() {
std::string activeName = module->programSelector.getSection(section).getCurrentProgramName();
std::string_view activeName = module->programSelector.getSection(section).getCurrentProgramName();
tooltip = new ui::Tooltip;
tooltip->text = activeName;
APP->scene->addChild(tooltip);
@@ -839,7 +844,7 @@ struct NoisePlethoraWidget : ModuleWidget {
menu->addChild(createSubmenuItem(string::f("Bank %d: %s", i + 1, bankAliases[i].c_str()), currentBank == i ? CHECKMARK_STRING : "", [ = ](Menu * menu) {
for (int j = 0; j < getBankForIndex(i).getSize(); ++j) {
const bool currentProgramAndBank = (currentProgram == j) && (currentBank == i);
const std::string algorithmName = getBankForIndex(i).getProgramName(j);
std::string_view algorithmName = getBankForIndex(i).getProgramName(j);

bool implemented = false;
for (auto item : MyFactory::Instance()->factoryFunctionRegistry) {
@@ -850,14 +855,14 @@ struct NoisePlethoraWidget : ModuleWidget {
}

if (implemented) {
menu->addChild(createMenuItem(algorithmName, currentProgramAndBank ? CHECKMARK_STRING : "",
menu->addChild(createMenuItem(algorithmName.data(), currentProgramAndBank ? CHECKMARK_STRING : "",
[ = ]() {
module->setAlgorithm(sectionId, algorithmName);
}));
}
else {
// placeholder text (greyed out)
menu->addChild(createMenuLabel(algorithmName));
menu->addChild(createMenuLabel(algorithmName.data()));
}
}
}));
@@ -874,4 +879,4 @@ struct NoisePlethoraWidget : ModuleWidget {
};


Model* modelNoisePlethora = createModel<NoisePlethora, NoisePlethoraWidget>("NoisePlethora");
Model* modelNoisePlethora = createModel<NoisePlethora, NoisePlethoraWidget>("NoisePlethora");

+ 11
- 11
src/Octaves.cpp View File

@@ -115,12 +115,10 @@ struct Octaves : Module {
const int numActivePolyphonyEngines = getNumActivePolyphonyEngines();

// work out active outputs
const std::vector<int> connectedOutputs = getConnectedOutputs();
if (connectedOutputs.size() == 0) {
const int highestOutput = getMaxConnectedOutput();
if (highestOutput == -1) {
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) {

@@ -200,8 +198,10 @@ struct Octaves : Module {
}
} // end of polyphony loop

for (int connectedOutput : connectedOutputs) {
outputs[OUT_01F_OUTPUT + connectedOutput].setChannels(numActivePolyphonyEngines);
for (int c = 0; c < NUM_OUTPUTS; c++) {
if (outputs[OUT_01F_OUTPUT + c].isConnected()) {
outputs[OUT_01F_OUTPUT + c].setChannels(numActivePolyphonyEngines);
}
}
}

@@ -219,14 +219,14 @@ struct Octaves : Module {
return activePolyphonyEngines;
}

std::vector<int> getConnectedOutputs() {
std::vector<int> connectedOutputs;
int getMaxConnectedOutput() {
int maxChans = -1;
for (int c = 0; c < NUM_OUTPUTS; c++) {
if (outputs[OUT_01F_OUTPUT + c].isConnected()) {
connectedOutputs.push_back(c);
maxChans = c;
}
}
return connectedOutputs;
return maxChans;
}

json_t* dataToJson() override {
@@ -333,4 +333,4 @@ struct OctavesWidget : ModuleWidget {
}
};

Model* modelOctaves = createModel<Octaves, OctavesWidget>("Octaves");
Model* modelOctaves = createModel<Octaves, OctavesWidget>("Octaves");

+ 1
- 1
src/noise-plethora/plugins/Banks.cpp View File

@@ -14,7 +14,7 @@ Bank::Bank(const BankElem& p1, const BankElem& p2, const BankElem& p3,
: programs{p1, p2, p3, p4, p5, p6, p7, p8, p9, p10}
{ }

const std::string Bank::getProgramName(int i) {
std::string_view Bank::getProgramName(int i) {
if (i >= 0 && i < programsPerBank) {
return programs[i].name;
}


+ 2
- 1
src/noise-plethora/plugins/Banks.hpp View File

@@ -1,6 +1,7 @@
#pragma once

#include <string>
#include <string_view>
#include <memory>
#include <array>

@@ -30,7 +31,7 @@ struct Bank {
const BankElem& p7 = defaultElem, const BankElem& p8 = defaultElem,
const BankElem& p9 = defaultElem, const BankElem& p10 = defaultElem);

const std::string getProgramName(int i);
std::string_view getProgramName(int i);
float getProgramGain(int i);

int getSize();


+ 1
- 1
src/noise-plethora/plugins/ProgramSelector.hpp View File

@@ -68,7 +68,7 @@ public:
return program.setValue(p, getBankForIndex(getBank()).getSize());
}

const std::string getCurrentProgramName() {
const std::string_view getCurrentProgramName() {
return getBankForIndex(getBank()).getProgramName(getProgram());
}



+ 1
- 0
src/plugin.cpp View File

@@ -31,4 +31,5 @@ void init(rack::Plugin *p) {
p->addModel(modelMidiThing);
p->addModel(modelVoltio);
p->addModel(modelOctaves);
p->addModel(modelBypass);
}

+ 8
- 0
src/plugin.hpp View File

@@ -32,6 +32,7 @@ extern Model* modelBurst;
extern Model* modelMidiThing;
extern Model* modelVoltio;
extern Model* modelOctaves;
extern Model* modelBypass;

struct Knurlie : SvgScrew {
Knurlie() {
@@ -240,6 +241,13 @@ struct Davies1900hWhiteKnobEndless : Davies1900hKnob {
}
};

template <typename TBase = WhiteLight>
struct VeryLargeSimpleLight : TBase {
VeryLargeSimpleLight() {
this->box.size = mm2px(math::Vec(7, 7));
}
};

inline int unsigned_modulo(int a, int b) {
return ((a % b) + b) % b;
}


Loading…
Cancel
Save