| @@ -1,5 +1,5 @@ | |||||
| #include "plugin.hpp" | #include "plugin.hpp" | ||||
| #include "ChowDSP.hpp" | |||||
| float aliasSuppressedSaw(const float* phases, float pw) { | float aliasSuppressedSaw(const float* phases, float pw) { | ||||
| float sawBuffer[3]; | float sawBuffer[3]; | ||||
| @@ -90,14 +90,6 @@ struct Octaves : Module { | |||||
| OUT_08F_OUTPUT, | OUT_08F_OUTPUT, | ||||
| OUT_16F_OUTPUT, | OUT_16F_OUTPUT, | ||||
| OUT_32F_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 | OUTPUTS_LEN | ||||
| }; | }; | ||||
| enum LightId { | enum LightId { | ||||
| @@ -106,9 +98,15 @@ struct Octaves : Module { | |||||
| bool limitPW = true; | bool limitPW = true; | ||||
| bool removePulseDC = false; | bool removePulseDC = false; | ||||
| bool adaa = false; | |||||
| int oversamplingIndex = 0; | |||||
| static const int NUM_OUTPUTS = 6; | 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() { | Octaves() { | ||||
| config(PARAMS_LEN, INPUTS_LEN, OUTPUTS_LEN, LIGHTS_LEN); | config(PARAMS_LEN, INPUTS_LEN, OUTPUTS_LEN, LIGHTS_LEN); | ||||
| @@ -146,124 +144,110 @@ struct Octaves : Module { | |||||
| configOutput(OUT_08F_OUTPUT, "x8F"); | configOutput(OUT_08F_OUTPUT, "x8F"); | ||||
| configOutput(OUT_16F_OUTPUT, "x16F"); | configOutput(OUT_16F_OUTPUT, "x16F"); | ||||
| configOutput(OUT_32F_OUTPUT, "x32F"); | 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 { | 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; | pitch += params[OCTAVE_PARAM].getValue() - 3; | ||||
| float freq = dsp::FREQ_C4 * dsp::exp2_taylor5(pitch); | float freq = dsp::FREQ_C4 * dsp::exp2_taylor5(pitch); | ||||
| // -1 to +1 | // -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; | const float pulseWidthLimit = limitPW ? 0.05f : 0.0f; | ||||
| // pwm in [-0.25 : +0.25] | // 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()) { | 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* dataToJson() override { | ||||
| json_t* rootJ = json_object(); | json_t* rootJ = json_object(); | ||||
| json_object_set_new(rootJ, "removePulseDC", json_boolean(removePulseDC)); | json_object_set_new(rootJ, "removePulseDC", json_boolean(removePulseDC)); | ||||
| json_object_set_new(rootJ, "limitPW", json_boolean(limitPW)); | 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; | return rootJ; | ||||
| } | } | ||||
| @@ -279,27 +263,14 @@ struct Octaves : Module { | |||||
| limitPW = json_boolean_value(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"); | json_t* oversamplingIndexJ = json_object_get(rootJ, "oversamplingIndex"); | ||||
| if (oversamplingIndexJ) { | if (oversamplingIndexJ) { | ||||
| oversamplingIndex = json_integer_value(oversamplingIndexJ); | oversamplingIndex = json_integer_value(oversamplingIndexJ); | ||||
| onSampleRateChange(); | onSampleRateChange(); | ||||
| } | } | ||||
| json_t* adaaJ = json_object_get(rootJ, "adaa"); | |||||
| if (adaaJ) { | |||||
| adaa = json_boolean_value(adaaJ); | |||||
| } | |||||
| } | } | ||||
| }; | }; | ||||
| struct OctavesWidget : ModuleWidget { | struct OctavesWidget : ModuleWidget { | ||||
| OctavesWidget(Octaves* module) { | OctavesWidget(Octaves* module) { | ||||
| setModule(module); | setModule(module); | ||||
| @@ -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(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(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 { | void appendContextMenu(Menu* menu) override { | ||||
| @@ -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"); | Model* modelOctaves = createModel<Octaves, OctavesWidget>("Octaves"); | ||||