Skip to content

Commit

Permalink
Switch to simpler oversampling version
Browse files Browse the repository at this point in the history
Range knob
  • Loading branch information
hemmer committed Mar 25, 2024
1 parent e696dea commit 8066bc2
Showing 1 changed file with 77 additions and 122 deletions.
199 changes: 77 additions & 122 deletions src/Octaves.cpp
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
#include "plugin.hpp"

#include "ChowDSP.hpp"

float aliasSuppressedSaw(const float* phases, float pw) {
float sawBuffer[3];
Expand Down Expand Up @@ -90,14 +90,6 @@ struct Octaves : Module {
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 {
Expand All @@ -106,9 +98,15 @@ struct Octaves : Module {

bool limitPW = true;
bool removePulseDC = false;
bool adaa = false;
int oversamplingIndex = 0;
static const int NUM_OUTPUTS = 6;
const float ranges[3] = {4.f, 1.f, 1.f / 12.f}; // full, octave, semitone

float phase = 0.f; // phase for core waveform, in [0, 1]
chowdsp::VariableOversampling<6, float> oversampler[NUM_OUTPUTS]; // uses a 2*6=12th order Butterworth filter
int oversamplingIndex = 1; // default is 2^oversamplingIndex == x2 oversampling

DCBlocker blockDCFilter; // optionally block DC with RC filter @ ~22 Hz
dsp::SchmittTrigger syncTrigger; // for hard sync

Octaves() {
config(PARAMS_LEN, INPUTS_LEN, OUTPUTS_LEN, LIGHTS_LEN);
Expand Down Expand Up @@ -146,124 +144,110 @@ struct Octaves : Module {
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;
// calculate up/downsampling rates
onSampleRateChange();
}

HardClipperADAA<float> hardClipper[NUM_OUTPUTS];
void onSampleRateChange() override {
float sampleRate = APP->engine->getSampleRate();
for (int c = 0; c < NUM_OUTPUTS; c++) {
oversampler[c].setOversamplingIndex(oversamplingIndex);
oversampler[c].reset(sampleRate);
}
blockDCFilter.setFrequency(22.05 / sampleRate);
}


void process(const ProcessArgs& args) override {
const int rangeIndex = params[RANGE_PARAM].getValue();

float pitch = params[TUNE_PARAM].getValue() + inputs[VOCT1_INPUT].getVoltage() + inputs[VOCT2_INPUT].getVoltage();
float pitch = ranges[rangeIndex] * 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 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 < NUM_OUTPUTS; 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 pwm = 2 * clamp(0.5 - params[PWM_PARAM].getValue() + 0.5 * pwmCV, -0.5f + pulseWidthLimit, 0.5f - pulseWidthLimit);

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;
const int oversamplingRatio = oversampler[0].getOversamplingRatio();

// 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);
// work out active outputs
std::vector<int> connectedOutputs = getConnectedOutputs();
if (connectedOutputs.size() == 0) {
return;
}
// only process up to highest active channel
const int highestOutput = *std::max_element(connectedOutputs.begin(), connectedOutputs.end());
const float deltaPhase = freq * args.sampleTime / oversamplingRatio;

float outForOctave = dpwOrder1;
// process sync
if (syncTrigger.process(inputs[SYNC_INPUT].getVoltage())) {
phase = 0.5f;
}

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;
for (int i = 0; i < oversamplingRatio; i++) {

phase += deltaPhase;
phase -= std::floor(phase);

float saw = aliasSuppressedSaw(phases, pwm);
float sawOffset = aliasSuppressedOffsetSaw(phases, pwm);
float denominatorInv = 0.25 / (effectiveDeltaPhase * effectiveDeltaPhase);
float dpwOrder3 = gain * (sawOffset - saw) * denominatorInv;
float sum = 0.f;
for (int c = 0; c <= highestOutput; 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 effectivePhase = n * std::fmod(phase, 1 / 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;

const float pulseDCOffset = (!removePulseDC) * 4.f * pwm * gain;
dpwOrder3 += pulseDCOffset;
const float waveTri = 1.0 - 2.0 * std::abs(2.f * effectivePhase - 1.0);
// build square from triangle + comparator
const float waveSquare = (waveTri > pwm) ? +1 : -1;

sum += waveSquare * gain;
sum = clamp(sum, -1.f, 1.f);

outForOctave = dpwOrder3;
if (outputs[OUT_01F_OUTPUT + c].isConnected()) {
oversampler[c].getOSBuffer()[i] = sum;
sum = 0.f;
}
}

} // end of oversampling loop

sum += outForOctave;
if (adaa) {
sum = hardClipper[c].process(sum);
}
else {
sum = clamp(sum, -1.f, 1.f);
}

// only downsample required channels
for (int c = 0; c <= highestOutput; c++) {
if (outputs[OUT_01F_OUTPUT + c].isConnected()) {
outputs[OUT_01F_OUTPUT + c].setVoltage(5 * sum);
sum = 0.f;
}
// downsample (if required)
float out = (oversamplingRatio > 1) ? oversampler[c].downsample() : oversampler[c].getOSBuffer()[0];

if (false) {
float x = 3 * std::sin(2 * M_PI * effectivePhase);
outputs[OUT_OUTPUT].setVoltage(clamp(x, -1.f, 1.f));
if (removePulseDC) {
out = blockDCFilter.process(out);
}

//float y = hardClipper.process(x);
//outputs[OUT2_OUTPUT].setVoltage(y);
outputs[OUT_01F_OUTPUT + c].setVoltage(5.f * out);
}


}

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

}

std::vector<int> getConnectedOutputs() {
std::vector<int> 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, "forceNaive", json_boolean(forceNaive));
json_object_set_new(rootJ, "adaa", json_boolean(adaa));
// TODO:
// json_object_set_new(rootJ, "oversamplingIndex", json_integer(oversampler[0].getOversamplingIndex()));
json_object_set_new(rootJ, "oversamplingIndex", json_integer(oversampler[0].getOversamplingIndex()));
return rootJ;
}

Expand All @@ -279,27 +263,14 @@ struct Octaves : Module {
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();
}

json_t* adaaJ = json_object_get(rootJ, "adaa");
if (adaaJ) {
adaa = json_boolean_value(adaaJ);
}
}
};




struct OctavesWidget : ModuleWidget {
OctavesWidget(Octaves* module) {
setModule(module);
Expand Down Expand Up @@ -340,16 +311,6 @@ struct OctavesWidget : ModuleWidget {
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 {
Expand All @@ -375,13 +336,7 @@ struct OctavesWidget : ModuleWidget {
}
));

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

menu->addChild(createBoolPtrMenuItem("ADAADAA", "", &module->adaa));


}
};


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

0 comments on commit 8066bc2

Please sign in to comment.