commit 643798c60569e04d2f6b3196c20b2a3ac1b3e3fd
parent 01bcf8294df5f09050fb9dc9d40be647f2ea0af1
Author: Matt Demanett <matt@demanett.net>
Date: Sun, 4 Jul 2021 18:31:33 -0400
VCO, LVCO, SINE, PULSE, XCO: add DC offset correction to pulse outputs by defualt, with context menu option to disable. #176
Diffstat:
11 files changed, 77 insertions(+), 18 deletions(-)
diff --git a/README-prerelease.md b/README-prerelease.md
@@ -78,6 +78,8 @@ The main frequency knob is calibrated in volts, from -4 to +6, corresponding to
In linear mode, the frequency 1000HZ times the pitch voltage (as determined by the knob plus V/OCT CV) -- at 0V, the frequency is zero, and the oscillator stops. In slow mode, it tracks at 1HZ times the pitch voltage. Negative voltages will realize the same output frequency as the corresponding positive voltage (the oscillator runs backwards). Use with with an FM input to create strange waveforms.
+The context menu option "DC offset correction", on by default, removes DC offset from the outputs. Presently, this only affects the square output when the pulse width is set to anything besides 50%. When this is enabled, and viewing the output on a scope, the waveform will move up and down relative to 0V as the pulse width is changed -- this is the DC offset removal in action. When disabled, the waveform stays centered on 0V, which is useful if using the output as CV.
+
_Polyphony:_ <a href="#polyphony">Polyphonic</a>, with channels defined by the V/OCT input. The poly port can be changed to the FM input on the context menu.
#### <a name="lvco"></a> LVCO
@@ -110,6 +112,8 @@ Includes all the features of VCO, adding:
- A mix knob/CV to control the level of the wave in the mix (waves are output at full level at their individual outputs). The mix knob/CV responses are linear in amplitude.
- A CV input for FM depth.
+The context menu option "DC offset correction" works as documented on <a href="#vco">VCO</a>.
+
_Polyphony:_ <a href="#polyphony">Polyphonic</a>, with channels defined by the V/OCT input.
#### <a name="additator"></a> ADDITATOR
diff --git a/src/LVCO.cpp b/src/LVCO.cpp
@@ -43,17 +43,17 @@ void LVCO::modulateChannel(int c) {
switch (_wave) {
case SQUARE_WAVE: {
e.squareActive = true;
- e.square.setPulseWidth(e.squarePulseWidthSL.next(0.5f));
+ e.square.setPulseWidth(e.squarePulseWidthSL.next(0.5f), _dcCorrection);
break;
}
case PULSE_25_WAVE: {
e.squareActive = true;
- e.square.setPulseWidth(e.squarePulseWidthSL.next(0.25f));
+ e.square.setPulseWidth(e.squarePulseWidthSL.next(0.25f), _dcCorrection);
break;
}
case PULSE_10_WAVE: {
e.squareActive = true;
- e.square.setPulseWidth(e.squarePulseWidthSL.next(0.1f));
+ e.square.setPulseWidth(e.squarePulseWidthSL.next(0.1f), _dcCorrection);
break;
}
default: {
@@ -82,7 +82,7 @@ void LVCO::processChannel(const ProcessArgs& args, int c) {
outputs[OUT_OUTPUT].setVoltage(e.squareOut + e.sawOut + e.triangleOut + e.sineOut, c);
}
-struct LVCOWidget : BGModuleWidget {
+struct LVCOWidget : VCOBaseModuleWidget {
static constexpr int hp = 3;
LVCOWidget(LVCO* module) {
@@ -145,6 +145,8 @@ struct LVCOWidget : BGModuleWidget {
p->addItem(OptionMenuItem("V/OCT input", [m]() { return m->_polyInputID == LVCO::PITCH_INPUT; }, [m]() { m->_polyInputID = LVCO::PITCH_INPUT; }));
p->addItem(OptionMenuItem("FM input", [m]() { return m->_polyInputID == LVCO::FM_INPUT; }, [m]() { m->_polyInputID = LVCO::FM_INPUT; }));
OptionsMenuItem::addToMenu(p, menu);
+
+ VCOBaseModuleWidget::contextMenu(menu);
}
};
diff --git a/src/Pulse.cpp b/src/Pulse.cpp
@@ -43,7 +43,7 @@ void Pulse::modulateChannel(int c) {
pw *= 1.0f - 2.0f * e.square.minPulseWidth;
pw *= 0.5f;
pw += 0.5f;
- e.square.setPulseWidth(e.squarePulseWidthSL.next(pw));
+ e.square.setPulseWidth(e.squarePulseWidthSL.next(pw), _dcCorrection);
}
void Pulse::processChannel(const ProcessArgs& args, int c) {
@@ -53,7 +53,7 @@ void Pulse::processChannel(const ProcessArgs& args, int c) {
outputs[OUT_OUTPUT].setVoltage(_engines[c]->squareOut, c);
}
-struct PulseWidget : BGModuleWidget {
+struct PulseWidget : VCOBaseModuleWidget {
static constexpr int hp = 3;
PulseWidget(Pulse* module) {
@@ -91,6 +91,8 @@ struct PulseWidget : BGModuleWidget {
auto m = dynamic_cast<Pulse*>(module);
assert(m);
menu->addChild(new BoolOptionMenuItem("Linear frequency mode", [m]() { return &m->_linearMode; }));
+
+ VCOBaseModuleWidget::contextMenu(menu);
}
};
diff --git a/src/Sine.cpp b/src/Sine.cpp
@@ -63,17 +63,17 @@ void Sine::modulateChannel(int c) {
}
case SQUARE_WAVE: {
e.squareActive = true;
- e.square.setPulseWidth(e.squarePulseWidthSL.next(0.5f));
+ e.square.setPulseWidth(e.squarePulseWidthSL.next(0.5f), _dcCorrection);
break;
}
case PULSE_25_WAVE: {
e.squareActive = true;
- e.square.setPulseWidth(e.squarePulseWidthSL.next(0.25f));
+ e.square.setPulseWidth(e.squarePulseWidthSL.next(0.25f), _dcCorrection);
break;
}
case PULSE_10_WAVE: {
e.squareActive = true;
- e.square.setPulseWidth(e.squarePulseWidthSL.next(0.1f));
+ e.square.setPulseWidth(e.squarePulseWidthSL.next(0.1f), _dcCorrection);
break;
}
default: {
@@ -98,7 +98,7 @@ void Sine::processChannel(const ProcessArgs& args, int c) {
outputs[OUT_OUTPUT].setVoltage(_outputScale * (e.squareOut + e.sawOut + e.triangleOut + e.sineOut), c);
}
-struct SineWidget : BGModuleWidget {
+struct SineWidget : VCOBaseModuleWidget {
static constexpr int hp = 3;
SineWidget(Sine* module) {
@@ -159,6 +159,8 @@ struct SineWidget : BGModuleWidget {
p->addItem(OptionMenuItem("V/OCT input", [m]() { return m->_polyInputID == Sine::PITCH_INPUT; }, [m]() { m->_polyInputID = Sine::PITCH_INPUT; }));
p->addItem(OptionMenuItem("FM input", [m]() { return m->_polyInputID == Sine::FM_INPUT; }, [m]() { m->_polyInputID = Sine::FM_INPUT; }));
OptionsMenuItem::addToMenu(p, menu);
+
+ VCOBaseModuleWidget::contextMenu(menu);
}
};
diff --git a/src/VCO.cpp b/src/VCO.cpp
@@ -34,7 +34,7 @@ void VCO::modulateChannel(int c) {
pw *= 1.0f - 2.0f * e.square.minPulseWidth;
pw *= 0.5f;
pw += 0.5f;
- e.square.setPulseWidth(e.squarePulseWidthSL.next(pw));
+ e.square.setPulseWidth(e.squarePulseWidthSL.next(pw), _dcCorrection);
}
}
@@ -52,7 +52,7 @@ void VCO::processChannel(const ProcessArgs& args, int c) {
outputs[SINE_OUTPUT].setVoltage(e.sineOut, c);
}
-struct VCOWidget : BGModuleWidget {
+struct VCOWidget : VCOBaseModuleWidget {
static constexpr int hp = 10;
VCOWidget(VCO* module) {
@@ -107,6 +107,8 @@ struct VCOWidget : BGModuleWidget {
p->addItem(OptionMenuItem("V/OCT input", [m]() { return m->_polyInputID == VCO::PITCH_INPUT; }, [m]() { m->_polyInputID = VCO::PITCH_INPUT; }));
p->addItem(OptionMenuItem("FM input", [m]() { return m->_polyInputID == VCO::FM_INPUT; }, [m]() { m->_polyInputID = VCO::FM_INPUT; }));
OptionsMenuItem::addToMenu(p, menu);
+
+ VCOBaseModuleWidget::contextMenu(menu);
}
};
diff --git a/src/XCO.cpp b/src/XCO.cpp
@@ -2,6 +2,8 @@
#include "XCO.hpp"
#include "dsp/pitch.hpp"
+#define DC_CORRECTION "dc_correction"
+
float XCO::XCOFrequencyParamQuantity::offset() {
auto xco = dynamic_cast<XCO*>(module);
return xco->_slowMode ? xco->_slowModeOffset : 0.0f;
@@ -56,6 +58,18 @@ void XCO::sampleRateChange() {
}
}
+json_t* XCO::toJson(json_t* root) {
+ json_object_set_new(root, DC_CORRECTION, json_boolean(_dcCorrection));
+ return root;
+}
+
+void XCO::fromJson(json_t* root) {
+ json_t* dc = json_object_get(root, DC_CORRECTION);
+ if (dc) {
+ _dcCorrection = json_boolean_value(dc);
+ }
+}
+
bool XCO::active() {
return (
outputs[MIX_OUTPUT].isConnected() ||
@@ -109,7 +123,7 @@ void XCO::modulateChannel(int c) {
pw *= 1.0f - 2.0f * e.square.minPulseWidth;
pw *= 0.5f;
pw += 0.5f;
- e.square.setPulseWidth(e.squarePulseWidthSL.next(pw));
+ e.square.setPulseWidth(e.squarePulseWidthSL.next(pw), _dcCorrection);
float saturation = params[SAW_SATURATION_PARAM].getValue();
if (inputs[SAW_SATURATION_INPUT].isConnected()) {
@@ -389,6 +403,12 @@ struct XCOWidget : BGModuleWidget {
addOutput(createOutput<Port24>(sineOutputPosition, module, XCO::SINE_OUTPUT));
addOutput(createOutput<Port24>(mixOutputPosition, module, XCO::MIX_OUTPUT));
}
+
+ void contextMenu(Menu* menu) override {
+ auto m = dynamic_cast<XCO*>(module);
+ assert(m);
+ menu->addChild(new BoolOptionMenuItem("DC offset correction", [m]() { return &m->_dcCorrection; }));
+ }
};
Model* modelXCO = bogaudio::createModel<XCO, XCOWidget>("Bogaudio-XCO", "XCO", "Oscillator", "Oscillator", "Polyphonic");
diff --git a/src/XCO.hpp b/src/XCO.hpp
@@ -123,6 +123,7 @@ struct XCO : BGModule {
float _oversampleThreshold = 0.0f;
bool _slowMode = false;
bool _fmLinearMode = false;
+ bool _dcCorrection = true;
Engine* _engines[maxChannels] {};
struct XCOFrequencyParamQuantity : FrequencyParamQuantity {
@@ -152,6 +153,8 @@ struct XCO : BGModule {
void reset() override;
void sampleRateChange() override;
+ json_t* toJson(json_t* root) override;
+ void fromJson(json_t* root) override;
bool active() override;
int channels() override;
void addChannel(int c) override;
diff --git a/src/dsp/oscillator.cpp b/src/dsp/oscillator.cpp
@@ -192,11 +192,12 @@ float SquareOscillator::nextForPhase(phase_t phase) {
}
-void BandLimitedSquareOscillator::setPulseWidth(float pw) {
- if (_pulseWidthInput == pw) {
+void BandLimitedSquareOscillator::setPulseWidth(float pw, bool dcCorrection) {
+ if (_pulseWidthInput == pw && _dcCorrection == dcCorrection) {
return;
}
_pulseWidthInput = pw;
+ _dcCorrection = dcCorrection;
if (pw >= maxPulseWidth) {
pw = maxPulseWidth;
@@ -212,12 +213,14 @@ void BandLimitedSquareOscillator::setPulseWidth(float pw) {
else {
_offset = -(1.0f - 2.0f * pw);
}
+
+ _dcOffset = _dcCorrection ? 1.0f - 2.0f * pw : 0.0f;
}
float BandLimitedSquareOscillator::nextForPhase(phase_t phase) {
float sample = -BandLimitedSawOscillator::nextForPhase(phase);
sample += BandLimitedSawOscillator::nextForPhase(phase - _pulseWidth);
- return sample + _offset;
+ return sample + _offset + _dcOffset;
}
diff --git a/src/dsp/oscillator.hpp b/src/dsp/oscillator.hpp
@@ -237,9 +237,11 @@ struct SquareOscillator : Phasor {
struct BandLimitedSquareOscillator : BandLimitedSawOscillator {
const float minPulseWidth = 0.03f;
const float maxPulseWidth = 1.0f - minPulseWidth;
- float _pulseWidthInput;
+ float _pulseWidthInput= -1.0f;
+ bool _dcCorrection = false;
phase_delta_t _pulseWidth;
float _offset;
+ float _dcOffset;
BandLimitedSquareOscillator(
float sampleRate = 1000.0f,
@@ -252,7 +254,7 @@ struct BandLimitedSquareOscillator : BandLimitedSawOscillator {
setPulseWidth(0.5f);
}
- void setPulseWidth(float pw);
+ void setPulseWidth(float pw, bool dcCorrection = true);
float nextForPhase(phase_t phase) override;
};
diff --git a/src/vco_base.cpp b/src/vco_base.cpp
@@ -2,6 +2,7 @@
#include "dsp/pitch.hpp"
#define POLY_INPUT "poly_input"
+#define DC_CORRECTION "dc_correction"
float VCOBase::VCOFrequencyParamQuantity::offset() {
auto vco = dynamic_cast<VCOBase*>(module);
@@ -77,6 +78,7 @@ void VCOBase::sampleRateChange() {
json_t* VCOBase::toJson(json_t* root) {
json_object_set_new(root, POLY_INPUT, json_integer(_polyInputID));
+ json_object_set_new(root, DC_CORRECTION, json_boolean(_dcCorrection));
return root;
}
@@ -85,6 +87,11 @@ void VCOBase::fromJson(json_t* root) {
if (p) {
_polyInputID = json_integer_value(p);
}
+
+ json_t* dc = json_object_get(root, DC_CORRECTION);
+ if (dc) {
+ _dcCorrection = json_boolean_value(dc);
+ }
}
int VCOBase::channels() {
@@ -209,3 +216,10 @@ void VCOBase::processChannel(const ProcessArgs& args, int c) {
e.sineOut = e.sineActive ? (amplitude * e.sine.nextFromPhasor(e.phasor, phaseOffset + e.additionalPhaseOffset)) : 0.0f;
}
+
+
+void VCOBaseModuleWidget::contextMenu(Menu* menu) {
+ auto m = dynamic_cast<VCOBase*>(module);
+ assert(m);
+ menu->addChild(new BoolOptionMenuItem("DC offset correction", [m]() { return &m->_dcCorrection; }));
+}
diff --git a/src/vco_base.hpp b/src/vco_base.hpp
@@ -64,6 +64,7 @@ struct VCOBase : BGModule {
int _syncInputID;
int _fmInputID;
int _polyInputID;
+ bool _dcCorrection = true;
struct VCOFrequencyParamQuantity : FrequencyParamQuantity {
float offset() override;
@@ -92,4 +93,8 @@ struct VCOBase : BGModule {
void processChannel(const ProcessArgs& args, int c) override;
};
+struct VCOBaseModuleWidget : BGModuleWidget {
+ void contextMenu(Menu* menu) override;
+};
+
} // namespace bogaudio