commit b45d6b93453d0ff72018acf23b28e4f874713a40
parent 9e1d3d4789e56c0bf72032363fa6b74d779bbcf6
Author: Matt Demanett <matt@demanett.net>
Date: Sun, 2 Feb 2025 17:06:55 -0500
Enable linear interpolation for all oscillator sine wave outputs, with an option for FM-OP. #225
Diffstat:
6 files changed, 45 insertions(+), 6 deletions(-)
diff --git a/README-prerelease.md b/README-prerelease.md
@@ -167,6 +167,10 @@ A sine-wave oscillator and simple synth voice designed to allow patching up the
Anti-aliasing techniques are applied when feedback or external FM are in use. Either condition for anti-aliasing can be disabled on the context menu. Prior to version 1.1.36, due to a long-standing bug, there was no anti-aliasing for external FM, unless feedback was also on. To get that behavior back, **the true vintage FM-OP sound**, disable "Anti-alias external FM" on the menu.
+As of version 2.6.46, menu option "Oscillator mode" sets the quality of the oscillator's output sine wave:
+ - Option "Classic", the default, is the classic FM-OP sound. The sine is produced from a wavetable, without interpolation. This adds small extra harmonics to the output.
+ - Option "Clean" enables interpolation, yielding a nearly-pure sine wave.
+
_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/FMOp.cpp b/src/FMOp.cpp
@@ -2,6 +2,9 @@
#include "FMOp.hpp"
#include "dsp/pitch.hpp"
+#define INTERPOLATION "interpolation"
+#define INTERPOLATION_VALUE_ON "on"
+#define INTERPOLATION_VALUE_OFF "off"
#define LINEAR_LEVEL "linearLevel"
#define ANTIALIAS_FEEDBACK "antialias_feedback"
#define ANTIALIAS_DEPTH "antialias_depth"
@@ -57,6 +60,7 @@ void FMOp::Engine::sampleRateChange() {
}
json_t* FMOp::saveToJson(json_t* root) {
+ json_object_set_new(root, INTERPOLATION, json_string(_interpolation == SineTableOscillator::INTERPOLATION_ON ? INTERPOLATION_VALUE_ON : INTERPOLATION_VALUE_OFF));
json_object_set_new(root, LINEAR_LEVEL, json_boolean(_linearLevel));
json_object_set_new(root, ANTIALIAS_FEEDBACK, json_boolean(_antiAliasFeedback));
json_object_set_new(root, ANTIALIAS_DEPTH, json_boolean(_antiAliasDepth));
@@ -64,6 +68,14 @@ json_t* FMOp::saveToJson(json_t* root) {
}
void FMOp::loadFromJson(json_t* root) {
+ json_t* i = json_object_get(root, INTERPOLATION);
+ if (i) {
+ const char *s = json_string_value(i);
+ if (strcmp(s, INTERPOLATION_VALUE_ON) == 0) {
+ _interpolation = SineTableOscillator::INTERPOLATION_ON;
+ }
+ }
+
json_t* ll = json_object_get(root, LINEAR_LEVEL);
if (ll) {
_linearLevel = json_is_true(ll);
@@ -174,6 +186,8 @@ void FMOp::modulateChannel(int c) {
if (inputs[LEVEL_INPUT].isConnected()) {
e.level *= clamp(inputs[LEVEL_INPUT].getPolyVoltage(c) / 10.0f, 0.0f, 1.0f);
}
+
+ e.sineTable.setInterpolation(_interpolation);
}
void FMOp::processAlways(const ProcessArgs& args) {
@@ -346,6 +360,12 @@ struct FMOpWidget : BGModuleWidget {
void contextMenu(Menu* menu) override {
auto fmop = dynamic_cast<FMOp*>(module);
assert(fmop);
+
+ OptionsMenuItem* om = new OptionsMenuItem("Oscillator mode");
+ om->addItem(OptionMenuItem("Classic (extra harmonics)", [fmop]() { return fmop->_interpolation == SineTableOscillator::INTERPOLATION_OFF; }, [fmop]() { fmop->_interpolation = SineTableOscillator::INTERPOLATION_OFF; }));
+ om->addItem(OptionMenuItem("Clean (pure sine)", [fmop]() { return fmop->_interpolation == SineTableOscillator::INTERPOLATION_ON; }, [fmop]() { fmop->_interpolation = SineTableOscillator::INTERPOLATION_ON; }));
+ OptionsMenuItem::addToMenu(om, menu);
+
menu->addChild(new BoolOptionMenuItem("Linear level response", [fmop]() { return &fmop->_linearLevel; }));
menu->addChild(new BoolOptionMenuItem("Anti-alias feedback", [fmop]() { return &fmop->_antiAliasFeedback; }));
diff --git a/src/FMOp.hpp b/src/FMOp.hpp
@@ -83,6 +83,7 @@ struct FMOp : BGModule {
void sampleRateChange();
};
+ SineTableOscillator::Interpolation _interpolation = SineTableOscillator::INTERPOLATION_OFF;
bool _linearLevel = false;
bool _antiAliasFeedback = true;
bool _antiAliasDepth = true;
diff --git a/src/PEQ14XR.hpp b/src/PEQ14XR.hpp
@@ -27,8 +27,6 @@ struct PEQ14XR : ExpanderModule<PEQ14ExpanderMessage, ExpandableModule<PEQ14Expa
struct BandOscillator {
Phasor _phasor;
TriangleOscillator _oscillator;
- // SineTableOscillator _oscillator;
- // BandLimitedSawOscillator _oscillator;
inline void setSampleRate(float sr) { _phasor.setSampleRate(sr); }
inline void setFrequency(float f) { _phasor.setFrequency(f); }
diff --git a/src/dsp/oscillator.cpp b/src/dsp/oscillator.cpp
@@ -59,9 +59,13 @@ float Phasor::nextForPhase(phase_t phase) {
}
+void TablePhasor::setInterpolation(Interpolation interpolation) {
+ _interpolation = interpolation;
+}
+
float TablePhasor::nextForPhase(phase_t phase) {
phase %= cyclePhase;
- if (_tableLength >= 1024) {
+ if (_interpolation == INTERPOLATION_OFF || (_interpolation == INTERPOLATION_DEFAULT && _tableLength >= 1024)) {
int i = (((((uint64_t)phase) << 16) / cyclePhase) * _tableLength) >> 16;
i %= _tableLength;
return _table.value(i);
diff --git a/src/dsp/oscillator.hpp b/src/dsp/oscillator.hpp
@@ -102,20 +102,31 @@ struct Phasor : OscillatorGenerator {
};
struct TablePhasor : Phasor {
+ enum Interpolation {
+ INTERPOLATION_DEFAULT,
+ INTERPOLATION_ON,
+ INTERPOLATION_OFF
+ };
+
const Table& _table;
int _tableLength;
+ Interpolation _interpolation;
TablePhasor(
const Table& table,
double sampleRate = 1000.0f,
- double frequency = 100.0f
+ double frequency = 100.0f,
+ Interpolation interpolation = INTERPOLATION_DEFAULT
)
: Phasor(sampleRate, frequency)
, _table(table)
, _tableLength(table.length())
+ , _interpolation(interpolation)
{
}
+ void setInterpolation(Interpolation interpolation);
+
float nextForPhase(phase_t phase) override;
};
@@ -151,9 +162,10 @@ struct SineOscillator : OscillatorGenerator {
struct SineTableOscillator : TablePhasor {
SineTableOscillator(
float sampleRate = 1000.0f,
- float frequency = 100.0f
+ float frequency = 100.0f,
+ Interpolation interpolation = INTERPOLATION_ON
)
- : TablePhasor(StaticSineTable::table(), sampleRate, frequency)
+ : TablePhasor(StaticSineTable::table(), sampleRate, frequency, interpolation)
{
}
};