BogaudioModules

BogaudioModules for VCV Rack
Log | Files | Refs | README | LICENSE

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:
MREADME-prerelease.md | 6++++++
Msrc/XCO.cpp | 49+++++++++++++++++++++++++++++++++++++++++++++++++
Msrc/XCO.hpp | 13+++++++++++++
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 {