Control rate parameter sounding crunchy

Hello everyone

It’s me again with a potentially silly question on parameters for UGens. I am experiencing something that I simply don’t understand. I have a simple comb filter UGen which has an audio input, a delaytime, a feedback amount, interpolation type and maxdelay parameter.

I have setup two different calculation functions. One for the case where delaytime is control rate, and one if it is audio rate.

If I set my comb filter to use cubic interpolation and modulate the delaytime with an audio rate ugen like LFPar.ar, it sounds great. No artifacts. If I then use the exact same LFPar ugen to modulate it with the exact same settings but with LFPar as a .kr, my comb filter UGen sounds crunchy and artifacty. I’ve tried different smoothing strategies but this sounds like it is beyond smoothing and just me misunderstanding something.

My .sc class looks like this:

NoComb : UGen {
	*ar { |input, delaytime=0.5, feedback=0.25, interpolation=2, maxdelay=1|
		^this.multiNew('audio', input, delaytime, feedback, interpolation, maxdelay);
	}
}

And the two calculation functions (note I use an enum to keep track of input indices):

// Audio rate delaytime
void NoComb::next_ak(int nSamples) {
  float *outbuf = out(0);

  m_delay.setFeedbackamount(in0(Feedback));

  for (int i = 0; i < nSamples; ++i) {
    m_delay.setDelaytime(in(DelayTime)[i]);

    outbuf[i] = m_delay.process(in(Input)[i]);
  }
}

// Control rate delaytime
void NoComb::next_kk(int nSamples) {
  const float *input = in(Input);
  float *outbuf = out(0);
  m_delay.setFeedbackamount(in0(Feedback));

  m_delay.setDelaytime(in(DelayTime)[0]);

  for (int i = 0; i < nSamples; ++i) {
    outbuf[i] = m_delay.process(input[i]);
  }
}

The input rate of my delaytime parameter is checked in the constructor and the appropriate calculation function is set there:

if (!m_delay.allocSuccesful()) {
    mCalcFunc = make_calc_function<NoComb, &NoComb::clear>();
    clear(1);
  } else {
    if (isAudioRateIn(DelayTime)) {
      mCalcFunc = make_calc_function<NoComb, &NoComb::next_ak>();
      next_ak(1);
    } else {
      mCalcFunc = make_calc_function<NoComb, &NoComb::next_kk>();
      next_kk(1);
    }
  }

What am I missing? I’ve been scratching my head over this for a while and cannot understand what the problem seems to be. Thanks for your help once again!

Alright, I solved this finally. The problem was (of course) that when the input parameter for delaytime is control rate, it will cause staircase-y jumps in the delaytime when modulated at control rate (once per sample block). I solved this by interpolating slowly inside of the for loop from the delaytime value in the previous block to the current delaytime:

// Control rate delay time
void NoComb::next_kk(int nSamples) {
  const float *input = in(Input);
  float *outbuf = out(0);
  const float delaytimeParam = in(DelayTime)[0];

  m_delay.setFeedbackamount(in0(Feedback));

  for (int i = 0; i < nSamples; ++i) {

    // Smooth delaytime
    delaytime_state += (delaytimeParam - delaytime_state) * 0.025f;
    m_delay.setDelaytime(delaytime_state);

    outbuf[i] = m_delay.process(input[i]);
  }
}

Ah, I had forgotten to answer this. Interpolating kr parameters is standard throughout the core UGens – just about everywhere that a slope is calculated.

hjh

Do you have a good example of this? Would be nice with something for reference. Thanks!

But there are probably hundreds of examples. You could open almost any UGen source file and look for “slope.”

hjh

1 Like

Thanks! This is very helpful

There is also a function called makeSlope in SC_PlugIn.hpp. I’ve never used it and I don’t know of any examples, but it looks cool. I report the relevant parts:

 template <typename FloatType> struct SlopeSignal {
        SlopeSignal(FloatType value, FloatType slope): value(value), slope(slope) {}

        FloatType consume() {
            FloatType ret = value;
            value += slope;
            return ret;
        }

        FloatType value, slope;
};

template <typename FloatType> inline SlopeSignal<FloatType> makeSlope(FloatType next, FloatType last) const {
        return SlopeSignal<FloatType>(last, calcSlope(next, last));
}

    /// calculate slope value
template <typename FloatType> FloatType calcSlope(FloatType next, FloatType prev) const {
        const Unit* unit = this;
        return CALCSLOPE(next, prev);
}

I might well be wrong about this, but guess you can use it like this:

  • at the beginning of kk you create the slope:
    SlopeSignal delayTime = makeSlope(in(DelayTime)[0], mPrevDelayTime)
  • in the loop you consume it:
    m_delay.setDelayTime(delayTime.consume())
  • at the end of kk you need to cache mPrevDelayTime = delayTime.value
1 Like

Thanks this looks nice! I think this is pretty much what I am doing now myself manually but might as well use this nice function. Thanks :slight_smile:

You were right @elgiano - thanks! This works (pun extremely intended) smoothly:


// Control rate delaytime
void NoDelay::next_kk(int nSamples) {
  // Output
  float *outbuf = out(0);
  delayunit.setFeedbackamount(in0(Feedback));

  // Smooth control rate delaytime between loop iterations
  SlopeSignal<float> delayTime = makeSlope(in(DelayTime)[0], m_delaytime_state);

  for (int i = 0; i < nSamples; ++i) {
    // Smooth delaytime parameter
    delayunit.setDelaytime(delayTime.consume());

    outbuf[i] = delayunit.process(in(Input)[i]);
  };

  // Cache delay time
  m_delaytime_state = delayTime.value;
}