commit e37b2d406dce0e7b2d93373c500c9831c25b3fee
parent 1958033b1b680ce8b71f8126c4f964aad06467f0
Author: Matt Demanett <matt@demanett.net>
Date: Sun, 12 Jun 2022 20:06:39 -0400
XCO: add scaling/clipping options for the mix output. This defaults on, which may affect existing patches. #201
Diffstat:
3 files changed, 68 insertions(+), 0 deletions(-)
diff --git a/README-prerelease.md b/README-prerelease.md
@@ -127,6 +127,12 @@ Includes all the features of VCO, adding:
The context menu option "DC offset correction" works as documented on <a href="#vco">VCO</a>.
+The context menu option "Mix output processing" sets how the mix output is scaled or clipped. VCV Rack modules generally [should not output signals exceeding +/-12 volts](https://vcvrack.com/manual/VoltageStandards#Output-Saturation), and oscillators typically output +/-5 volts. However, XCO's mix output combines four oscillators, and can easily exceed these values. "Mix output processing" sets how to deal with this:
+ - "Scaled to +/-5V" (default): the output is directly scaled down to +/-5V (like a regular oscillator) when it would otherwise exceed that range (it will not scale up; it is possible to reduce the output below this range with the MIX controls). The scaling is updated once per oscillator cycle, and abrupt parameter changes can confuse it; in this case hard clipping takes over.
+ - "Saturated": a saturator (soft clipper) keeps the output within +/-12V.
+ - "Hard clipped": the output is simply clipped at +/-12V.
+ - "None": no scaling or limiting is applied to the output, ignoring the voltage standards. This was how XCO always behaved prior to version 2.*.41. Older patches should restore this mode if affected by the addition of these options.
+
_Polyphony:_ <a href="#polyphony">polyphonic</a>, with channels defined by the V/OCT input.
_When <a href="#bypassing">bypassed</a>:_ no output.
diff --git a/src/XCO.cpp b/src/XCO.cpp
@@ -3,6 +3,7 @@
#include "dsp/pitch.hpp"
#define DC_CORRECTION "dc_correction"
+#define CLIPPING_MODE "clipping_mode"
float XCO::XCOFrequencyParamQuantity::offset() {
auto xco = dynamic_cast<XCO*>(module);
@@ -60,6 +61,7 @@ void XCO::sampleRateChange() {
json_t* XCO::saveToJson(json_t* root) {
json_object_set_new(root, DC_CORRECTION, json_boolean(_dcCorrection));
+ json_object_set_new(root, CLIPPING_MODE, json_integer(_clippingMode));
return root;
}
@@ -68,6 +70,14 @@ void XCO::loadFromJson(json_t* root) {
if (dc) {
_dcCorrection = json_boolean_value(dc);
}
+
+ json_t* c = json_object_get(root, CLIPPING_MODE);
+ if (c) {
+ _clippingMode = (Clipping)json_integer_value(c);
+ if (_clippingMode != SOFT_CLIPPING && _clippingMode != HARD_CLIPPING && _clippingMode != NO_CLIPPING) {
+ _clippingMode = COMP_CLIPPING;
+ }
+ }
}
bool XCO::active() {
@@ -289,6 +299,37 @@ void XCO::processChannel(const ProcessArgs& args, int c) {
mix += e.sawMixSL.next(e.sawMix) * sawOut;
mix += e.triangleMixSL.next(e.triangleMix) * triangleOut;
mix += e.sineMixSL.next(e.sineMix) * sineOut;
+
+ switch (_clippingMode) {
+ case COMP_CLIPPING: {
+ Phasor::phase_t cycle = e.phasor._phase / Phasor::cyclePhase;
+ if (e.lastCycle != cycle) {
+ e.lastCycle = cycle;
+ e.mixScale = 1.0f / ((-e.minMix + e.maxMix) / 10.0f);
+ e.minMix = 0.0f;
+ e.maxMix = 0.0f;
+ } else if (mix < e.minMix) {
+ e.minMix = mix;
+ } else if (mix > e.maxMix) {
+ e.maxMix = mix;
+ }
+ if (e.mixScale < 1.0f) {
+ mix *= e.mixScale;
+ }
+ mix = clamp(mix, -12.0f, 12.0f);
+ break;
+ }
+ case SOFT_CLIPPING: {
+ mix = e.saturator.next(mix);
+ break;
+ }
+ case HARD_CLIPPING: {
+ mix = clamp(mix, -12.0f, 12.0f);
+ break;
+ }
+ case NO_CLIPPING:;
+ }
+
outputs[MIX_OUTPUT].setChannels(_channels);
outputs[MIX_OUTPUT].setVoltage(mix, c);
}
@@ -407,7 +448,15 @@ struct XCOWidget : BGModuleWidget {
void contextMenu(Menu* menu) override {
auto m = dynamic_cast<XCO*>(module);
assert(m);
+
menu->addChild(new BoolOptionMenuItem("DC offset correction", [m]() { return &m->_dcCorrection; }));
+
+ OptionsMenuItem* c = new OptionsMenuItem("Mix output processing");
+ c->addItem(OptionMenuItem("Scaled to 10Vpp", [m]() { return m->_clippingMode == XCO::COMP_CLIPPING; }, [m]() { m->_clippingMode = XCO::COMP_CLIPPING; }));
+ c->addItem(OptionMenuItem("Saturated", [m]() { return m->_clippingMode == XCO::SOFT_CLIPPING; }, [m]() { m->_clippingMode = XCO::SOFT_CLIPPING; }));
+ c->addItem(OptionMenuItem("Hard clipped", [m]() { return m->_clippingMode == XCO::HARD_CLIPPING; }, [m]() { m->_clippingMode = XCO::HARD_CLIPPING; }));
+ c->addItem(OptionMenuItem("None", [m]() { return m->_clippingMode == XCO::NO_CLIPPING; }, [m]() { m->_clippingMode = XCO::NO_CLIPPING; }));
+ OptionsMenuItem::addToMenu(c, menu);
}
};
diff --git a/src/XCO.hpp b/src/XCO.hpp
@@ -62,6 +62,13 @@ struct XCO : BGModule {
NUM_OUTPUTS
};
+ enum Clipping {
+ NO_CLIPPING = 0,
+ COMP_CLIPPING,
+ SOFT_CLIPPING,
+ HARD_CLIPPING
+ };
+
struct Engine {
static constexpr int oversample = 8;
@@ -81,6 +88,10 @@ struct XCO : BGModule {
float sawMix = 1.0f;
float triangleMix = 1.0f;
float sineMix = 1.0f;
+ float lastCycle = 0.0f;
+ float minMix = 0.0f;
+ float maxMix = 0.0f;
+ float mixScale = 0.25;
Phasor phasor;
BandLimitedSquareOscillator square;
@@ -96,6 +107,7 @@ struct XCO : BGModule {
float triangleBuffer[oversample];
float sineBuffer[oversample];
PositiveZeroCrossing syncTrigger;
+ Saturator saturator;
bogaudio::dsp::SlewLimiter fmDepthSL;
bogaudio::dsp::SlewLimiter squarePulseWidthSL;
@@ -124,6 +136,7 @@ struct XCO : BGModule {
bool _slowMode = false;
bool _fmLinearMode = false;
bool _dcCorrection = true;
+ Clipping _clippingMode = COMP_CLIPPING;
Engine* _engines[maxChannels] {};
struct XCOFrequencyParamQuantity : FrequencyParamQuantity {