Browse Source

Add Octaves, remove experimental EvenVCO implementation

pull/47/head
hemmer 1 year ago
parent
commit
700a5cce98
8 changed files with 2743 additions and 2195 deletions
  1. +3
    -0
      CHANGELOG.md
  2. +11
    -12
      plugin.json
  3. +0
    -1850
      res/panels/EvenVCObeta.svg
  4. +2383
    -0
      res/panels/Octaves.svg
  5. +0
    -331
      src/EvenVCO2.cpp
  6. +344
    -0
      src/Octaves.cpp
  7. +1
    -1
      src/plugin.cpp
  8. +1
    -1
      src/plugin.hpp

+ 3
- 0
CHANGELOG.md View File

@@ -4,6 +4,9 @@
## v2.6.0 (in progress)
* Midi Thing 2
* Initial release
* Octaves
* Initial release


## v2.5.0
* Burst


+ 11
- 12
plugin.json View File

@@ -23,18 +23,6 @@
"Polyphonic"
]
},
{
"slug": "EvenVCO2",
"name": "Even VCO (beta)",
"description": "Oscillator including even-harmonic waveform",
"manualUrl": "https://www.befaco.org/even-vco/",
"modularGridUrl": "https://www.modulargrid.net/e/befaco-even-vco-",
"tags": [
"VCO",
"Hardware clone",
"Polyphonic"
]
},
{
"slug": "Rampage",
"name": "Rampage",
@@ -331,6 +319,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"
]
}
]
}

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


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


+ 0
- 331
src/EvenVCO2.cpp View File

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

using simd::float_4;

struct EvenVCO2 : Module {
enum ParamIds {
OCTAVE_PARAM,
TUNE_PARAM,
PWM_PARAM,
NUM_PARAMS
};
enum InputIds {
PITCH1_INPUT,
PITCH2_INPUT,
FM_INPUT,
SYNC_INPUT,
PWM_INPUT,
NUM_INPUTS
};
enum OutputIds {
TRI_OUTPUT,
SINE_OUTPUT,
EVEN_OUTPUT,
SAW_OUTPUT,
SQUARE_OUTPUT,
NUM_OUTPUTS
};


float_4 phase[4] = {};
dsp::TSchmittTrigger<float_4> syncTrigger[4];
bool removePulseDC = true;
bool limitPW = true;

EvenVCO2() {
config(NUM_PARAMS, NUM_INPUTS, NUM_OUTPUTS);
configParam(OCTAVE_PARAM, -5.0, 4.0, 0.0, "Octave", "'", 0.5);
getParamQuantity(OCTAVE_PARAM)->snapEnabled = true;
configParam(TUNE_PARAM, -7.0, 7.0, 0.0, "Tune", " semitones");
configParam(PWM_PARAM, -1.0, 1.0, 0.0, "Pulse width");

configInput(PITCH1_INPUT, "Pitch 1");
configInput(PITCH2_INPUT, "Pitch 2");
configInput(FM_INPUT, "FM");
configInput(SYNC_INPUT, "Sync");
configInput(PWM_INPUT, "Pulse Width Modulation");

configOutput(TRI_OUTPUT, "Triangle");
configOutput(SINE_OUTPUT, "Sine");
configOutput(EVEN_OUTPUT, "Even");
configOutput(SAW_OUTPUT, "Sawtooth");
configOutput(SQUARE_OUTPUT, "Square");

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

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

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

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_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
}

return (sawBuffer[0] - 2.0 * sawBuffer[1] + sawBuffer[2]);
}

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

return (sawBuffer[0] - 2.0 * sawBuffer[1] + sawBuffer[2]);
}

float_4 aliasSuppressedOffsetSaw(float_4* phases, float_4 pw) {
float_4 sawOffsetBuff[3];

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

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

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

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

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

// 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);

// 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]);

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) {

phase[c / 4] += deltaBasePhase;
// ensure within [0, 1]
phase[c / 4] -= simd::floor(phase[c / 4]);

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];

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

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;

osBufferTri[i] = simd::ifelse(lowFreqRegime, dpwOrder1, dpwOrder3);
}

if (outputs[SAW_OUTPUT].isConnected()) {
const float_4 dpwOrder1 = 2 * phase[c / 4] - 1.0;
const float_4 dpwOrder3 = aliasSuppressedSaw(phases) * denominatorInv;

osBufferSaw[i] = simd::ifelse(lowFreqRegime, dpwOrder1, dpwOrder3);
}

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

float_4 dpwOrder1 = simd::ifelse(phase[c / 4] < 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 = (saw - sawOffset) * denominatorInv + pulseDCOffset;

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);
outputs[SINE_OUTPUT].setChannels(channels);
outputs[EVEN_OUTPUT].setChannels(channels);
outputs[SAW_OUTPUT].setChannels(channels);
outputs[SQUARE_OUTPUT].setChannels(channels);
}


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

void dataFromJson(json_t* rootJ) override {
json_t* pulseDCJ = json_object_get(rootJ, "removePulseDC");
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();
}
}
};


struct EvenVCO2Widget : ModuleWidget {
EvenVCO2Widget(EvenVCO2* module) {
setModule(module);
setPanel(APP->window->loadSvg(asset::plugin(pluginInstance, "res/panels/EvenVCObeta.svg")));

addChild(createWidget<Knurlie>(Vec(15, 0)));
addChild(createWidget<Knurlie>(Vec(15, 365)));
addChild(createWidget<Knurlie>(Vec(15 * 6, 0)));
addChild(createWidget<Knurlie>(Vec(15 * 6, 365)));

addParam(createParam<BefacoBigKnob>(Vec(22, 32), module, EvenVCO2::OCTAVE_PARAM));
addParam(createParam<BefacoTinyKnob>(Vec(73, 131), module, EvenVCO2::TUNE_PARAM));
addParam(createParam<Davies1900hRedKnob>(Vec(16, 230), module, EvenVCO2::PWM_PARAM));

addInput(createInput<BefacoInputPort>(Vec(8, 120), module, EvenVCO2::PITCH1_INPUT));
addInput(createInput<BefacoInputPort>(Vec(19, 157), module, EvenVCO2::PITCH2_INPUT));
addInput(createInput<BefacoInputPort>(Vec(48, 183), module, EvenVCO2::FM_INPUT));
addInput(createInput<BefacoInputPort>(Vec(86, 189), module, EvenVCO2::SYNC_INPUT));

addInput(createInput<BefacoInputPort>(Vec(72, 236), module, EvenVCO2::PWM_INPUT));

addOutput(createOutput<BefacoOutputPort>(Vec(10, 283), module, EvenVCO2::TRI_OUTPUT));
addOutput(createOutput<BefacoOutputPort>(Vec(87, 283), module, EvenVCO2::SINE_OUTPUT));
addOutput(createOutput<BefacoOutputPort>(Vec(48, 306), module, EvenVCO2::EVEN_OUTPUT));
addOutput(createOutput<BefacoOutputPort>(Vec(10, 327), module, EvenVCO2::SAW_OUTPUT));
addOutput(createOutput<BefacoOutputPort>(Vec(87, 327), module, EvenVCO2::SQUARE_OUTPUT));
}

void appendContextMenu(Menu* menu) override {
EvenVCO2* module = dynamic_cast<EvenVCO2*>(this->module);
assert(module);

menu->addChild(new MenuSeparator());
menu->addChild(createSubmenuItem("Hardware compatibility", "",
[ = ](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();
}
));
}
};


Model* modelEvenVCO2 = createModel<EvenVCO2, EvenVCO2Widget>("EvenVCO2");

+ 344
- 0
src/Octaves.cpp View File

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


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


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,
OUT_OUTPUT,
OUT2_OUTPUT,
OUT_01F_OUTPUT_ALT,
OUT_02F_OUTPUT_ALT,
OUT_04F_OUTPUT_ALT,
OUT_08F_OUTPUT_ALT,
OUT_16F_OUTPUT_ALT,
OUT_32F_OUTPUT_ALT,
OUTPUTS_LEN
};
enum LightId {
LIGHTS_LEN
};

bool limitPW = true;
bool removePulseDC = false;
int oversamplingIndex = 0;

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, 4.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, 0.f, "Range", {"VCO: Full", "VCO: Octave", "VCO: Semitone"});
rangeParam->snapEnabled = true;

configParam(GAIN_01F_PARAM, 0.f, 1.f, 0.f, "Gain Fundamental");
configParam(GAIN_02F_PARAM, 0.f, 1.f, 0.f, "Gain x2 Fundamental");
configParam(GAIN_04F_PARAM, 0.f, 1.f, 0.f, "Gain x4 Fundamental");
configParam(GAIN_08F_PARAM, 0.f, 1.f, 0.f, "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");
configOutput(OUT_OUTPUT, "debug");
}

float phase = 0.f;
float phases[3];
bool forceNaive = false;

void process(const ProcessArgs& args) override {

float pitch = params[TUNE_PARAM].getValue() + inputs[VOCT1_INPUT].getVoltage() + inputs[VOCT2_INPUT].getVoltage();
pitch += params[OCTAVE_PARAM].getValue() - 3;
float freq = dsp::FREQ_C4 * dsp::exp2_taylor5(pitch);
// -1 to +1
float pwmCV = params[PWM_CV_PARAM].getValue() * clamp(inputs[PWM_INPUT].getVoltage() / 10.f, -1.f, 1.f);
const float pulseWidthLimit = limitPW ? 0.05f : 0.0f;
// pwm in [-0.25 : +0.25]
float pwm = clamp(0.5 - params[PWM_PARAM].getValue() + 0.5 * pwmCV, -0.5f + pulseWidthLimit, 0.5f - pulseWidthLimit);
pwm /= 2.0;


float deltaPhase = freq * args.sampleTime;
phase += deltaPhase;
phase -= std::floor(phase);

float sum = 0.f;
float sumNaive = 0.f;
for (int c = 0; c < 6; c++) {
// derive phases for higher octaves from base phase (this keeps things in sync!)
const float n = (float)(1 << c);
// this is on [0, 1]
const float effectivePhaseRaw = n * std::fmod(phase, 1 / n);
// this is on [0, 1], and offset in time by 0.25
const float effectivePhase = std::fmod(effectivePhaseRaw + 0.25, 1);

const float effectiveDeltaPhase = deltaPhase * n;
const float gainCV = clamp(inputs[GAIN_01F_INPUT + c].getNormalVoltage(10.f) / 10.f, 0.f, 1.0f);
const float gain = params[GAIN_01F_PARAM + c].getValue() * gainCV;

// 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 bool lowFreqRegime = forceNaive; //effectiveDeltaPhase < 1e-3 || forceNaive;


//float waveTri = 1.0 - 2.0 * std::abs(2.f * effectivePhase - 1.0);
// float dpwOrder1 = (waveTri > 2 * pwm - 1) ? 1.0 : -1.0;

float dpwOrder1 = gain * (effectivePhaseRaw > pwm + 0.25 && effectivePhaseRaw < 0.75 - pwm ? -1.0 : +1.0);
dpwOrder1 -= removePulseDC ? 2.f * (0.5f - pwm) : 0.f;

// dpwOrder1 = waveTri * gain;

sumNaive += dpwOrder1;

outputs[OUT_01F_OUTPUT_ALT + c].setVoltage(dpwOrder1);

float outForOctave = dpwOrder1;

if (!lowFreqRegime) {
phases[0] = effectivePhase - 2 * effectiveDeltaPhase + (effectivePhase < 2 * effectiveDeltaPhase ? 1.f : 0.f);
phases[1] = effectivePhase - 1 * effectiveDeltaPhase + (effectivePhase < 1 * effectiveDeltaPhase ? 1.f : 0.f);
phases[2] = effectivePhase;


float saw = aliasSuppressedSaw(phases, pwm);
float sawOffset = aliasSuppressedOffsetSaw(phases, pwm);
float denominatorInv = 0.25 / (effectiveDeltaPhase * effectiveDeltaPhase);
float dpwOrder3 = gain * (sawOffset - saw) * denominatorInv;

const float pulseDCOffset = (!removePulseDC) * 4.f * pwm * gain;
dpwOrder3 += pulseDCOffset;


outForOctave = dpwOrder3;
}


sum += outForOctave;
sum = clamp(sum, -1.f, 1.f);

if (outputs[OUT_01F_OUTPUT + c].isConnected()) {
outputs[OUT_01F_OUTPUT + c].setVoltage(5 * sum);
sum = 0.f;
}

if (c == 0) {
outputs[OUT_OUTPUT].setVoltage(effectivePhase);
float saw = aliasSuppressedSaw(phases, 2*pwm);
float sawOffset = aliasSuppressedOffsetSaw(phases, 2*pwm);
float denominatorInv = 0.25 / (effectiveDeltaPhase * effectiveDeltaPhase);
float dpwOrder3_ = gain * (-saw) * denominatorInv;

outputs[OUT2_OUTPUT].setVoltage(dpwOrder3_);
}


}

//outputs[OUT_OUTPUT].setVoltage(sum);
//outputs[OUT2_OUTPUT].setVoltage(phase > 0.5 ? +5 : -5);

}


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, "forceNaive", json_boolean(forceNaive));
// TODO:
// json_object_set_new(rootJ, "oversamplingIndex", json_integer(oversampler[0].getOversamplingIndex()));
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* forceNaiveJ = json_object_get(rootJ, "forceNaive");
if (forceNaiveJ) {
forceNaive = json_boolean_value(forceNaiveJ);
}

json_t* oversamplingIndexJ = json_object_get(rootJ, "oversamplingIndex");
if (oversamplingIndexJ) {
oversamplingIndex = json_integer_value(oversamplingIndexJ);
onSampleRateChange();
}
}
};




struct OctavesWidget : ModuleWidget {
OctavesWidget(Octaves* module) {
setModule(module);
setPanel(createPanel(asset::plugin(pluginInstance, "res/panels/Octaves.svg")));

addChild(createWidget<Knurlie>(Vec(RACK_GRID_WIDTH, 0)));
addChild(createWidget<Knurlie>(Vec(box.size.x - 2 * RACK_GRID_WIDTH, 0)));
addChild(createWidget<Knurlie>(Vec(RACK_GRID_WIDTH, RACK_GRID_HEIGHT - RACK_GRID_WIDTH)));
addChild(createWidget<Knurlie>(Vec(box.size.x - 2 * RACK_GRID_WIDTH, RACK_GRID_HEIGHT - RACK_GRID_WIDTH)));

addParam(createParamCentered<BefacoTinyKnobLightGrey>(mm2px(Vec(52.138, 15.037)), module, Octaves::PWM_CV_PARAM));
addParam(createParam<CKSSVert7>(mm2px(Vec(22.171, 30.214)), module, Octaves::OCTAVE_PARAM));
addParam(createParamCentered<BefacoTinyKnobLightGrey>(mm2px(Vec(10.264, 33.007)), module, Octaves::TUNE_PARAM));
addParam(createParamCentered<Davies1900hLargeRedKnob>(mm2px(Vec(45.384, 40.528)), module, Octaves::PWM_PARAM));
addParam(createParam<CKSSThreeHorizontal>(mm2px(Vec(6.023, 48.937)), module, Octaves::RANGE_PARAM));
addParam(createParam<BefacoSlidePotSmall>(mm2px(Vec(2.9830, 60.342)), module, Octaves::GAIN_01F_PARAM));
addParam(createParam<BefacoSlidePotSmall>(mm2px(Vec(12.967, 60.342)), module, Octaves::GAIN_02F_PARAM));
addParam(createParam<BefacoSlidePotSmall>(mm2px(Vec(22.951, 60.342)), module, Octaves::GAIN_04F_PARAM));
addParam(createParam<BefacoSlidePotSmall>(mm2px(Vec(32.936, 60.342)), module, Octaves::GAIN_08F_PARAM));
addParam(createParam<BefacoSlidePotSmall>(mm2px(Vec(42.920, 60.342)), module, Octaves::GAIN_16F_PARAM));
addParam(createParam<BefacoSlidePotSmall>(mm2px(Vec(52.905, 60.342)), module, Octaves::GAIN_32F_PARAM));

addInput(createInputCentered<BefacoInputPort>(mm2px(Vec(5.247, 15.181)), module, Octaves::VOCT1_INPUT));
addInput(createInputCentered<BefacoInputPort>(mm2px(Vec(15.282, 15.181)), module, Octaves::VOCT2_INPUT));
addInput(createInputCentered<BefacoInputPort>(mm2px(Vec(25.316, 15.181)), module, Octaves::SYNC_INPUT));
addInput(createInputCentered<BefacoInputPort>(mm2px(Vec(37.092, 15.135)), module, Octaves::PWM_INPUT));
addInput(createInputCentered<BefacoInputPort>(mm2px(Vec(5.247, 100.492)), module, Octaves::GAIN_01F_INPUT));
addInput(createInputCentered<BefacoInputPort>(mm2px(Vec(15.282, 100.492)), module, Octaves::GAIN_02F_INPUT));
addInput(createInputCentered<BefacoInputPort>(mm2px(Vec(25.316, 100.492)), module, Octaves::GAIN_04F_INPUT));
addInput(createInputCentered<BefacoInputPort>(mm2px(Vec(35.35, 100.492)), module, Octaves::GAIN_08F_INPUT));
addInput(createInputCentered<BefacoInputPort>(mm2px(Vec(45.384, 100.492)), module, Octaves::GAIN_16F_INPUT));
addInput(createInputCentered<BefacoInputPort>(mm2px(Vec(55.418, 100.492)), module, Octaves::GAIN_32F_INPUT));

addOutput(createOutputCentered<BefacoOutputPort>(mm2px(Vec(5.247, 113.508)), module, Octaves::OUT_01F_OUTPUT));
addOutput(createOutputCentered<BefacoOutputPort>(mm2px(Vec(15.282, 113.508)), module, Octaves::OUT_02F_OUTPUT));
addOutput(createOutputCentered<BefacoOutputPort>(mm2px(Vec(25.316, 113.508)), module, Octaves::OUT_04F_OUTPUT));
addOutput(createOutputCentered<BefacoOutputPort>(mm2px(Vec(35.35, 113.508)), module, Octaves::OUT_08F_OUTPUT));
addOutput(createOutputCentered<BefacoOutputPort>(mm2px(Vec(45.384, 113.508)), module, Octaves::OUT_16F_OUTPUT));
addOutput(createOutputCentered<BefacoOutputPort>(mm2px(Vec(55.418, 113.508)), module, Octaves::OUT_32F_OUTPUT));

addOutput(createOutputCentered<BefacoOutputPort>(mm2px(Vec(25.316, 106.508)), module, Octaves::OUT_OUTPUT));
addOutput(createOutputCentered<BefacoOutputPort>(mm2px(Vec(35.316, 106.508)), module, Octaves::OUT2_OUTPUT));

addOutput(createOutputCentered<BefacoOutputPort>(mm2px(Vec(5.247, 120.508)), module, Octaves::OUT_01F_OUTPUT_ALT));
addOutput(createOutputCentered<BefacoOutputPort>(mm2px(Vec(15.282, 120.508)), module, Octaves::OUT_02F_OUTPUT_ALT));
addOutput(createOutputCentered<BefacoOutputPort>(mm2px(Vec(25.316, 120.508)), module, Octaves::OUT_04F_OUTPUT_ALT));
addOutput(createOutputCentered<BefacoOutputPort>(mm2px(Vec(35.35, 120.508)), module, Octaves::OUT_08F_OUTPUT_ALT));
addOutput(createOutputCentered<BefacoOutputPort>(mm2px(Vec(45.384, 120.508)), module, Octaves::OUT_16F_OUTPUT_ALT));
addOutput(createOutputCentered<BefacoOutputPort>(mm2px(Vec(55.418, 120.508)), module, Octaves::OUT_32F_OUTPUT_ALT));

}

void appendContextMenu(Menu* menu) override {
Octaves* module = dynamic_cast<Octaves*>(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(createIndexSubmenuItem("Oversampling",
{"Off", "x2", "x4", "x8"},
[ = ]() {
return module->oversamplingIndex;
},
[ = ](int mode) {
module->oversamplingIndex = mode;
module->onSampleRateChange();
}
));

menu->addChild(createBoolPtrMenuItem("Force naive waveforms", "", &module->forceNaive));


}
};


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

+ 1
- 1
src/plugin.cpp View File

@@ -7,7 +7,6 @@ void init(rack::Plugin *p) {
pluginInstance = p;

p->addModel(modelEvenVCO);
p->addModel(modelEvenVCO2);
p->addModel(modelRampage);
p->addModel(modelABC);
p->addModel(modelSpringReverb);
@@ -31,4 +30,5 @@ void init(rack::Plugin *p) {
p->addModel(modelBurst);
p->addModel(modelMidiThing);
p->addModel(modelVoltio);
p->addModel(modelOctaves);
}

+ 1
- 1
src/plugin.hpp View File

@@ -8,7 +8,6 @@ using namespace rack;
extern Plugin* pluginInstance;

extern Model* modelEvenVCO;
extern Model* modelEvenVCO2;
extern Model* modelRampage;
extern Model* modelABC;
extern Model* modelSpringReverb;
@@ -32,6 +31,7 @@ extern Model* modelMotionMTR;
extern Model* modelBurst;
extern Model* modelMidiThing;
extern Model* modelVoltio;
extern Model* modelOctaves;

struct Knurlie : SvgScrew {
Knurlie() {


Loading…
Cancel
Save