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
- it seems that the BeatRepeater start a little before the (around 40 samples before)
- 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



