Beat repeater (Stutter effect)

Hi all,

I’m struggeling to implement a beat repeater (like the one of Looperator or Ableton Live beat repeat effect).

The purpose of this effect is, given a delay time, it will record during this delay time then will repeat it until the next beat.

For example if we have a 4/4 time signature, we will have:

input => | a b c d | e f g h | i j k l | m n o p |
output => | _ a a a | _ e e e | _ i i i | _ m m m |
(where “_” means the recording time where no output should be hear in full dry signal.)

I’ve almost finished but I get some weird clicks between each loop.

Here is the code I use.

void Repeater::process(const Context& context) {
  const auto ioblock = context.getOutputBlock();
  const auto channelCount = ioblock.getNumChannels();
  const auto sampleCount = ioblock.getNumSamples();

  updateIfNeeded();

  if (!isPlaying_) {
    for (auto& delayLine : delayLines_) {
      delayLine.reset();
    }
    return;
  }

  const auto dry = 1 - dryWet_.load();
  const auto wet = dryWet_.load();

  for (int channelIndex = 0; channelIndex < channelCount; channelIndex++) {
    auto channelData = ioblock.getChannelPointer(channelIndex);

    for (int sampleIndex = 0; sampleIndex < sampleCount; sampleIndex++) {

      positionInsideBeatInSampleVector_[channelIndex] =
      positionInsideBeatInSampleVector_[channelIndex] % repeatLengthInSample_;

      if (positionInsideBeatInSampleVector_[channelIndex] == 0) {
        // We start a new recording now so we reset the delay line recorder.
        delayLines_[channelIndex].reset();
      }

      const auto popedSample = delayLines_[channelIndex].popSample(0, delay_);

      const auto isRecording =
      shouldRecord(positionInsideBeatInSampleVector_[channelIndex]);

      const auto currentSample = channelData[sampleIndex];
      const auto sampleToPush = isRecording ? currentSample : popedSample;
      delayLines_[channelIndex].pushSample(0, sampleToPush);

      const auto outputSample = isRecording ? 0 : popedSample;

      if (!context.isBypassed) {
        channelData[sampleIndex] = (currentSample * dry) + (outputSample * wet);
      }

      positionInsideBeatInSampleVector_[channelIndex]++;
    }
  }
}


bool Repeater::shouldRecord(int samplePosition) {
  const auto recordFirstSample = recordSampleRange_.getStart();
  const auto recordLastSample = recordSampleRange_.getEnd();
  const auto shouldRecord =
  recordFirstSample <= samplePosition && samplePosition <= recordLastSample;
  return shouldRecord;
}

and how I calculate the ppqPosition:

void Repeater::update() {
  updateDelay();
  updateCurrentPositionInfo();

}

void Repeater::updateCurrentPositionInfo() {
  isPlaying_ = currentPositionInfo_.isPlaying;

  const auto beatLengthInSec = (60.0 / currentPositionInfo_.bpm);
  const auto beatLengthInSample = beatLengthInSec * sampleRate_;
  const auto factor = 1;
  beatLenghtInSample_ = beatLengthInSample * factor;
  repeatLengthInSample_ = beatLenghtInSample_; 

  double ppqPositionIntegral = 0;
  const auto ppqPositionFractional = modf(currentPositionInfo_.ppqPosition,
                                          &ppqPositionIntegral);

  for (auto& positionInsideBeatInSample : positionInsideBeatInSampleVector_) {
    positionInsideBeatInSample = ppqPositionFractional * beatLenghtInSample_;
  }

  // If go back to start of a loop.
  if (lastBeatIndex_ > ppqPositionIntegral) {
    for (auto& delayLine : delayLines_) {
      delayLine.reset();
    }
  }

  lastBeatIndex_ = ppqPositionIntegral;

}

void Repeater::updateDelay() {
  jassert(delayLines_.size() > 0);
  const auto previousDelay = delayLines_[0].getDelay();
  const auto delayInt = static_cast<int>(std::floor(delay_));
  recordSampleRange_.setEnd(delayInt);
}

I also made a comparaison with the BeatRepeater plugin of ableton and I see 2 main differences. I use a delay of 1/16 at 120 bpm with a sample rate of 44100 Hz

  1. it seems that the BeatRepeater start a little before the (around 40 samples before)
  2. it seems that the BeatRepeater has an interpolation (?) between each repeated loop.

Here are the BeatRepeater (top) vs my effect (bottom) signal with Audacity:

We clearly see issues at each loop cycle.

I don’t think I’m missing sample, so should I use a kind of interpolation between each frame?
Does anybody know how to resolve this kind of issue?

Thanks

1 Like

You need to overlap your loops a bit and apply a crossfade on them. What you are hearing are clicks due to a discontinuity in the waveform.

Thanks for the insight.
Maybe it’s the reason why the beat repeater from ableton starts at an earlier time than the expected delay time:


(Top ableton, bottom me)

Doing this could be a solution to avoid to have a latency.

I thought the delay line could handle it since there is interpolation between each poped samples:
I used the Lagrange3rd (but the Thiran didn’t work too).

But an interpolation doesn’t seem to be enough from what you said.

I’m pretty sure that’s the reason, they probably do 80ms crossfading, that’s why it starts 40 ms earlier.

Just to let you know, the crossfading was the solution. Thanks @gustav-scholda

2 Likes