ForumsSoftware ← Aalto - Zipper noise when modulating filter cutoff

I use Aalto a lot with my Manta controller, which can send pad pressure as polyphonic aftertouch. I just re-opened a patch I used a while ago (almost a year now) and now I'm hearing substantial zipper noise when I modulate the filter cutoff using poly aftertouch. I'm not sure if I just didn't notice it before or if something has changed.

To reproduce:
1. start with the Aalto Default patch
2. set the shape to sawtooth to get some harmonics
3. route polyphonic aftertouch to filter cutoff
4. crank up the input scaler on filter cutoff

Particularly on higher notes, modulating pressure will sweep the cutoff, and the noise sounds pretty bad.

I'm not sure what the right general approach is here, given that for the waveguide frequency I actually make use of the discontinuities that come from mapping the low-resolution MIDI data (the jumps sound awesome), so I wouldn't want everything to be all smoothed out. In that case though I'm doing the control through MIDI mapping in my DAW, so at least for me it would be a good fix to just add some smoothing to the polyphonic aftertouch signal within the Aalto patchbay.

Hey, bumping this thread since this issue is still pretty apparent in Aalto and Kaivo. I think the simple solution would be to apply some smoothing to incoming midi CC/aftertouch messages. Currently, Aalto and Kaivo are almost perfect with MPE, but the zippering noise can be pretty distracting with some parameters.

I appreciate the reminder. I'm not sure how I missed this the first time around, so sorry about that!

It's an easy fix if some params want to be smoothed and others don't. But in the case of the waveguide frequency some people might want this to be smoothed and others might not. A user-configurable smoothing frequency for each parameter would be too complicated, I think.

I could see an approach of smoothing changes, unless the parameter has not been changed for a short while. So if you start modulating gradually away from 0, the first change won't be a jump anyway and it will still sound smooth. But if the first modulation is a high value, it would immediately jump to that value, then begin smooth changes if values kept flowing in, as from poly aftertouch and so on.

Hey Randy, thanks so much for the response!

I like your thought process on the adaptive smoothing, but think that really a simple toggle in the settings between having incoming aftertouch/cc interpolation on/off would satisfy most users. For different smoothing methods inspiration, the opensource Surge vst has a setting for this with a few different interpolation modes which you could potentially easily port - though really a toggle-able simple linear interpolation would make a huge difference!

Personally, I dont think I'd ever want unsmoothed ccs unless I was trying to get a glitchy/steppy sound for some reason, in which case I wouldnt want any smoothing at all. Occams razor seems to apply :)

Hope you're doing well!

UPDATE: I dug around in the Surge source code and found the relevant bits here, I hope this helps (sorry for the lack of indents, the forum doesn't seem to be preserving them but here's a pastebin: https://pastebin.com/yiYaNK1X )

class ControllerModulationSource : public ModulationSource
{
  public:
    // Smoothing and Shaping Behaviors
    enum SmoothingMode
    {
        LEGACY = -1, // This is (1) the exponential backoff and (2) not streamed.
        SLOW_EXP,    // Legacy with a sigma clamp
        FAST_EXP,    // Faster Legacy with a sigma clamp
        FAST_LINE,   // Linearly move
        DIRECT       // Apply the value directly
    } smoothingMode = LEGACY;

    ControllerModulationSource()
    {
        target = 0.f;
        output = 0.f;
        bipolar = false;
        changed = true;
        smoothingMode = LEGACY;
    }
    ControllerModulationSource(SmoothingMode mode) : ControllerModulationSource()
    {
        smoothingMode = mode;
    }

    virtual ~ControllerModulationSource() {}
    void set_target(float f)
    {
        target = f;
        startingpoint = output;
        changed = true;
    }

    void init(float f)
    {
        target = f;
        output = f;
        startingpoint = f;
        changed = true;
    }

    void set_target01(float f, bool updatechanged = true)
    {
        if (bipolar)
            target = 2.f * f - 1.f;
        else
            target = f;
        startingpoint = output;
        if (updatechanged)
            changed = true;
    }

    virtual float get_output01(int i) override
    {
        if (bipolar)
            return 0.5f + 0.5f * output;
        return output;
    }

    virtual float get_target01()
    {
        if (bipolar)
            return 0.5f + 0.5f * target;
        return target;
    }

    virtual bool has_changed(bool reset)
    {
        if (changed)
        {
            if (reset)
                changed = false;
            return true;
        }
        return false;
    }

    virtual void reset() override
    {
        target = 0.f;
        output = 0.f;
        bipolar = false;
    }
    inline void processSmoothing(SmoothingMode mode, float sigma)
    {
        if (mode == LEGACY || mode == SLOW_EXP || mode == FAST_EXP)
        {
            float b = fabs(target - output);
            if (b < sigma &amp;&amp; mode != LEGACY)
            {
                output = target;
            }
            else
            {
                float a = (mode == FAST_EXP ? 0.99f : 0.9f) * 44100 * samplerate_inv * b;
                output = (1 - a) * output + a * target;
            }
            return;
        };
        if (mode == FAST_LINE)
        {
            /*
             * Apply a constant change until we get there.
             * Rate is set so we cover the entire range (0,1)
             * in 50 blocks at 44k
             */
            float sampf = samplerate / 44100;
            float da = (target - startingpoint) / (50 * sampf);
            float b = target - output;
            if (fabs(b) < fabs(da))
            {
                output = target;
            }
            else
            {
                output += da;
            }
        }
        if (mode == DIRECT)
        {
            output = target;
        }
    }
    virtual void process_block() override
    {
        processSmoothing(smoothingMode, smoothingMode == FAST_EXP ? 0.005f : 0.0025f);
    }

    virtual bool process_block_until_close(float sigma)
    {
        if (smoothingMode == LEGACY)
            processSmoothing(SLOW_EXP, sigma);
        else
            processSmoothing(smoothingMode, sigma);

        return (output != target); // continue
    }

    virtual bool is_bipolar() override { return bipolar; }
    virtual void set_bipolar(bool b) override { bipolar = b; }

    float target, startingpoint;
    int id; // can be used to assign the controller to a parameter id
    bool bipolar;
    bool changed;
};

And then, elsewhere in the code, the smoothing selected is applied to these midi signals:

        scene.modsources[ms_modwheel] = new ControllerModulationSource(storage.smoothingMode);
        scene.modsources[ms_breath] = new ControllerModulationSource(storage.smoothingMode);
        scene.modsources[ms_expression] = new ControllerModulationSource(storage.smoothingMode);
        scene.modsources[ms_sustain] = new ControllerModulationSource(storage.smoothingMode);
        scene.modsources[ms_aftertouch] = new ControllerModulationSource(storage.smoothingMode);
        scene.modsources[ms_pitchbend] = new ControllerModulationSource(storage.smoothingMode);
        scene.modsources[ms_lowest_key] = new ControllerModulationSource(storage.smoothingMode);
        scene.modsources[ms_highest_key] = new ControllerModulationSource(storage.smoothingMode);
        scene.modsources[ms_latest_key] = new ControllerModulationSource(storage.smoothingMode);

I appreciate your thoughts on smoothing. The code in this case does not help because it's not something I can just drop into my own framework. Finding the time is the hard part. I'll be making a big list of Aalto enhancements soon and this should be near the top.

I understand - thanks for the response :blush: