Help dealing with Aliasing?

Hi everyone, so… I managed to make an oscillator and implement some functions here. Great start! Now I’m finding that I overlooked aliasing! I’ve tried to implement some saturation as well (which i believe i did successfully) since I get some harmonics even for my sine wave. Now, you can start to see where I’m going with this…

After a certain frequency I start to encounter aliasing and the harmonics start to fold down. Not to mention this happens for my saw wave as well, obviously.

What are my options here? Oversampling? I’m a little unsure about anti aliasing filtering since I guess I’d be losing the harmonics introduced by saturation?? I took a peek at the fundamental WTOSC since I felt like that might approximate what I’m trying to do here (just without the square wave as part of the WT)… which might solve this aliasing issue? But I really have no idea how to start implementing that here.

I’ve done some C# but this is my first time doing any C++ so excuse if my implementation is super shoddy.

#include "plugin.hpp"
#include "Wavetable.hpp"


using simd::float_4;

struct Model_158 : Module {
    
    float phaseA = 0.f;  // Phase for Oscillator A
    float phaseB = 0.f;  // Phase for Oscillator B

    enum ParamId {
        OSC_A_PITCH_PARAM,
        FM_A_AMOUNT,
        WAVESHAPE_A,
        OSC_B_PITCH_PARAM,
        FM_B_AMOUNT,
        WAVESHAPE_B,
        PARAMS_LEN
    };
    enum InputId {
        IN_FM_A_INPUT,
        PITCH_A_INPUT_INPUT,
        IN_FM_B_INPUT,
        PITCH_B_INPUT_INPUT,
        INPUTS_LEN
    };
    enum OutputId {
        OUT_1_A_OUTPUT,
        OUT_2_A_OUTPUT,
        OUT_1_B_OUTPUT,
        OUT_2_B_OUTPUT,
        OUTPUTS_LEN
    };
    enum LightId {
        LIGHTS_LEN
    };

    Wavetable wavetable;
    float_4 phases[4] = {};
    float lastPos = 0.f;
    dsp::MinBlepGenerator<16, 16, float_4> syncMinBleps[4];

    Model_158() {
        config(PARAMS_LEN, INPUTS_LEN, OUTPUTS_LEN, LIGHTS_LEN);

        // Oscillator A parameters and outputs
        configParam(OSC_A_PITCH_PARAM, std::log2(5.f / dsp::FREQ_C4), std::log2(20000.f / dsp::FREQ_C4), 0.f, "Osc A Frequency");
        configParam(FM_A_AMOUNT, 0.f, 1.f, 0.f, "FM Amount A");
        configParam(WAVESHAPE_A, 0.f, 1.f, 0.5f, "Waveshape A");
        configInput(IN_FM_A_INPUT, "");
        configInput(PITCH_A_INPUT_INPUT, "");
        configOutput(OUT_1_A_OUTPUT, "Osc A Main Out");
        configOutput(OUT_2_A_OUTPUT, "Osc A Second Output");

        // Oscillator B parameters and outputs
        configParam(OSC_B_PITCH_PARAM, std::log2(5.f / dsp::FREQ_C4), std::log2(20000.f / dsp::FREQ_C4), 0.f, "Osc B Frequency");
        configParam(FM_B_AMOUNT, 0.f, 1.f, 0.f, "FM Amount B");
        configParam(WAVESHAPE_B, 0.f, 1.f, 0.5f, "Waveshape B");
        configInput(IN_FM_B_INPUT, "");
        configInput(PITCH_B_INPUT_INPUT, "");
        configOutput(OUT_1_B_OUTPUT, "Osc B Main Out");
        configOutput(OUT_2_B_OUTPUT, "Osc B Second Output");
    }



    float getWave(float index, float pos, float octave) {
        // Get wave indexes
        float indexF = index - std::trunc(index);
        size_t index0 = std::trunc(index);
        size_t index1 = (index0 + 1) % (wavetable.waveLen * wavetable.quality);
        // Get position indexes
        float posF = pos - std::trunc(pos);
        size_t pos0 = std::trunc(pos);
        size_t pos1 = pos0 + 1;
        // Get octave index
        // float octaveF = octave - std::trunc(octave);
        size_t octave0 = std::trunc(octave);
        octave0 = std::min(octave0, wavetable.octaves - 1);
        // size_t octave1 = octave0 + 1;

        // Linearly interpolate wave index
        float out = crossfade(wavetable.interpolatedAt(octave0, pos0, index0), wavetable.interpolatedAt(octave0, pos0, index1), indexF);
        // Interpolate octave
        // if (octaveF > 0.f && octave1 < wavetable.octaves) {
        //  float out1 = crossfade(wavetable.interpolatedAt(octave1, pos0, index0), wavetable.interpolatedAt(octave1, pos0, index1), indexF);
        //  out = crossfade(out, out1, octaveF);
        // }
        // Linearly interpolate position if needed
        if (posF > 0.f) {
            float out1 = crossfade(wavetable.interpolatedAt(octave0, pos1, index0), wavetable.interpolatedAt(octave0, pos1, index1), indexF);
            // Interpolate octave
            // if (octaveF > 0.f && octave1 < wavetable.octaves) {
            //  float out2 = crossfade(wavetable.interpolatedAt(octave1, pos1, index0), wavetable.interpolatedAt(octave1, pos1, index1), indexF);
            //  out1 = crossfade(out1, out2, octaveF);
            // }
            out = crossfade(out, out1, posF);
        }
        return out;
    }
    

    void process(const ProcessArgs& args) override {
        // Process Oscillator A
        float pitchA = params[OSC_A_PITCH_PARAM].getValue() + inputs[PITCH_A_INPUT_INPUT].getVoltage();
        float freqA = dsp::FREQ_C4 * std::pow(2.f, pitchA);
        float fmInputA = inputs[IN_FM_A_INPUT].getVoltage();
        float fmAmountA = params[FM_A_AMOUNT].getValue();
        freqA *= std::pow(2.f, fmAmountA * fmInputA);
        phaseA += freqA * args.sampleTime;
        if (phaseA >= 1.f)
            phaseA -= 1.f;

        float waveshapeA = params[WAVESHAPE_A].getValue();
        float waveformValueA;
        if (waveshapeA < 0.5) {
            float sineValueA = std::sin(2.f * M_PI * phaseA);
            float sawValueA = 2.f * (phaseA - std::floor(phaseA + 0.5));
            waveformValueA = (1.0 - 2.0 * waveshapeA) * sineValueA + (2.0 * waveshapeA) * sawValueA;
        } else {
            waveformValueA = 2.f * (phaseA - std::floor(phaseA + 0.5));
        }
        waveformValueA = std::tanh(waveformValueA);

        outputs[OUT_1_A_OUTPUT].setVoltage(5.f * waveformValueA);
        outputs[OUT_2_A_OUTPUT].setVoltage(5.f * waveformValueA);

        // Process Oscillator B (similar to Oscillator A)
        float pitchB = params[OSC_B_PITCH_PARAM].getValue() + inputs[PITCH_B_INPUT_INPUT].getVoltage();
        float freqB = dsp::FREQ_C4 * std::pow(2.f, pitchB);
        float fmInputB = inputs[IN_FM_B_INPUT].getVoltage();
        float fmAmountB = params[FM_B_AMOUNT].getValue();
        freqB *= std::pow(2.f, fmAmountB * fmInputB);
        phaseB += freqB * args.sampleTime;
        if (phaseB >= 1.f)
            phaseB -= 1.f;

        float waveshapeB = params[WAVESHAPE_B].getValue();
        float waveformValueB;
        if (waveshapeB < 0.5) {
            float sineValueB = std::sin(2.f * M_PI * phaseB);
            float sawValueB = 2.f * (phaseB - std::floor(phaseB + 0.5));
            waveformValueB = (1.0 - 2.0 * waveshapeB) * sineValueB + (2.0 * waveshapeB) * sawValueB;
        } else {
            waveformValueB = 2.f * (phaseB - std::floor(phaseB + 0.5));
        }
        waveformValueB = std::tanh(waveformValueB);

        outputs[OUT_1_B_OUTPUT].setVoltage(5.f * waveformValueB);
        outputs[OUT_2_B_OUTPUT].setVoltage(5.f * waveformValueB);
    }
};



struct Model_158Widget : ModuleWidget {
    Model_158Widget(Model_158* module) {
        setModule(module);
        setPanel(createPanel(asset::plugin(pluginInstance, "res/Model_158.svg")));

        addChild(createWidget<ScrewSilver>(Vec(RACK_GRID_WIDTH, 0)));
        addChild(createWidget<ScrewSilver>(Vec(box.size.x - 2 * RACK_GRID_WIDTH, 0)));
        addChild(createWidget<ScrewSilver>(Vec(RACK_GRID_WIDTH, RACK_GRID_HEIGHT - RACK_GRID_WIDTH)));
        addChild(createWidget<ScrewSilver>(Vec(box.size.x - 2 * RACK_GRID_WIDTH, RACK_GRID_HEIGHT - RACK_GRID_WIDTH)));

        // Use BuchlaKnob to create the oscillator frequency knob
        BuchlaKnobText* FreqknobA = createParam<BuchlaKnobText>(mm2px(Vec(4.893, 92.804)), module, Model_158::OSC_A_PITCH_PARAM);
        
        // Set the minimum and maximum angles for the knob graphic
        FreqknobA->minAngle = -0.75 * M_PI;
        FreqknobA->maxAngle = 0.75 * M_PI;
        addParam(FreqknobA);


        BuchlaKnobTiny* knobTiny = createParam<BuchlaKnobTiny>(mm2px(Vec(10.5, 62)), module, Model_158::FM_A_AMOUNT);
        
        
        addParam(knobTiny);
        addParam(createParam<BuchlaKnobTiny>(mm2px(Vec(10.5, 42)), module, Model_158::WAVESHAPE_A));


        addInput(createInputCentered<PJ301MPort>(mm2px(Vec(9.378, 27.808)), module, Model_158::IN_FM_A_INPUT));
        addInput(createInputCentered<PJ301MPort>(mm2px(Vec(20.795, 27.972)), module, Model_158::PITCH_A_INPUT_INPUT));

        addOutput(createOutputCentered<PJ301MPort>(mm2px(Vec(7.223, 15.733)), module, Model_158::OUT_1_A_OUTPUT));
        addOutput(createOutputCentered<PJ301MPort>(mm2px(Vec(25.223, 15.733)), module, Model_158::OUT_2_A_OUTPUT));

        //Oscilator B

         BuchlaKnobText* FreqknobB = createParam<BuchlaKnobText>(mm2px(Vec(4.893 + 40, 92.804)), module, Model_158::OSC_B_PITCH_PARAM);

        // Set the minimum and maximum angles for the knob graphic
        FreqknobB->minAngle = -0.75 * M_PI;
        FreqknobB->maxAngle = 0.75 * M_PI;
        addParam(FreqknobB);

        BuchlaKnobTiny* knobTinyB = createParam<BuchlaKnobTiny>(mm2px(Vec(10.5 + 40, 62)), module, Model_158::FM_B_AMOUNT);
        addParam(knobTinyB);

        addParam(createParam<BuchlaKnobTiny>(mm2px(Vec(10.5 + 40, 42)), module, Model_158::WAVESHAPE_B));

        addInput(createInputCentered<PJ301MPort>(mm2px(Vec(9.378 + 40, 27.808)), module, Model_158::IN_FM_B_INPUT));
        addInput(createInputCentered<PJ301MPort>(mm2px(Vec(20.795 + 40, 27.972)), module, Model_158::PITCH_B_INPUT_INPUT));

        addOutput(createOutputCentered<PJ301MPort>(mm2px(Vec(7.223 + 40, 15.733)), module, Model_158::OUT_1_B_OUTPUT));
        addOutput(createOutputCentered<PJ301MPort>(mm2px(Vec(25.223 + 40, 15.733)), module, Model_158::OUT_2_B_OUTPUT));
    }
};

Model* modelModel_158 = createModel<Model_158, Model_158Widget>("Model_158");


Read @Squinky’s papers on aliasing.

Here is a short article I wrote on how to evaluate aliasing using free VCV modules: Demo/aliasing2.md at main · squinkylabs/Demo · GitHub

Demo/docs/vco2.md at main · squinkylabs/Demo (github.com):

Now let’s address the aliasing. There are, broadly speaking, four well known ways to deal with aliasing, all of them used in some VCV modules:

  1. Oversampling
  2. minBlep
  3. Something else
  4. Do nothing

The first two techniques, oversampling and minBlep, are often used by VCV modules.

and lots more.

thanks for the plug! and, btw, in the repo is the source code for a minblep VCO, and for an oversampling clipper - full VCV modules. There are “better” techniques that can be used in some cases.

The anti-imaging filters used for oversampling are to filter out the stuff ABOVE the nyquist frequency, so it will not take out your good harmonics. But depending on how sophisticated your filters are you can make a “brick wall” at FS/2, or do something not as good and take out some highs.

I’m not really a DSP expert, so I just use a 6 pole butterworth filter at F/4, which does strip out some good stuff in the top octave.

All this stuff is much, much more difficult that writing a “naive” vco/saturator. There are plenty of modules that don’t try, although afaik all the popular ones do something to tame the aliasing.

3 Likes