This isn’t a Juce problem per se, it’s more of a maths problem. But I’m not that good at maths, so I could really use some help!
What I want to achieve is a smooth bpm transition as the user gradually turns a knob. By default, the new bpm frequency will take affect starting from the next beat, but I want the timing of the next beat to be interpolated so that it occurs at the right time. But I’m not quite able to picture what the “right” time is, nor how to calculate it.
Everything I’ve tried so far has sounded pretty wonky, with the timing of the next beat sounding pretty erratic. Can anyone help me work out the right formula?
I have the following parameters, all measured in samples since the start of runtime:
oldPeriod: the time period (60/bpm) that we’re transitioning away from
newPeriod: the time period we’re transitioning to
instantiationTime: the time at which the current / waiting beat was instantiated
executionTime: the time at which the current beat was originally set to execute
interruptionTime: the time at which the newPeriod message was received
I’m not working with Juce classes here, so let’s not worry about the mechanics of what’s happening–suffice to say that the system will recognize if I update executionTime.
Perhaps I’m misunderstanding the exact requirements, but assuming you want to “make a change” only at a rate appropriate to “newPeriod” + oldPeriod then it would be a case of using modulo to find how many samples to wait for?
Is it that you want a smooth rate of change (so interpolate between the last N rates) or just change to the “real” current value, but only do so without interrupting the current period? (ie an uninterrupted delay)
I’m looking for an “interrupted” option, so that the next beat is sped up or slowed down according to the new bpm. Here are two situations which it needs to be able to accommodate:
Suppose the current bpm is set to 2bpm. If the user now sets it to 60bpm just after one of the beats, they don’t have to wait for almost 30 seconds for the new setting to take effect, and it jumps to the right speed immediately.
If the user moves from 60bpm to 61bpm, the effect will be almost inaudible, regardless of when the new setting is sent.
I feel like there must be a formula that does this, and you’re right that it must use modulo. But I can’t grasp exactly what the formula is.
#include <stdio.h>
#include <math.h>
// Define PID constants
#define KP 0.1 // Proportional gain
#define KI 0.01 // Integral gain
#define KD 0.02 // Derivative gain
// Function to calculate PID control output
double calculatePID(double currentTempo, double destinationTempo, double *prevError, double *integral) {
double error = destinationTempo - currentTempo;
*integral += error;
double derivative = error - *prevError;
double output = KP * error + KI * (*integral) + KD * derivative;
*prevError = error;
return output;
}
int main() {
double currentTempo = 100.0; // Current tempo in BPM (beats per minute)
double destinationTempo = 120.0; // Destination tempo in BPM
double prevError = 0.0;
double integral = 0.0;
while (fabs(destinationTempo - currentTempo) > 0.01) { // Continue until the error is small
double controlOutput = calculatePID(currentTempo, destinationTempo, &prevError, &integral);
currentTempo += controlOutput;
// In a real MIDI application, you would use currentTempo to control the tempo of MIDI events.
// For simplicity, we'll print the current tempo here.
printf("Current Tempo: %.2f BPM\n", currentTempo);
}
printf("Reached the destination tempo: %.2f BPM\n", currentTempo);
return 0;
}
Oh yeah, GPT4 has been helping me a lot with this. But I couldn’t get exactly the result that I was looking for, and then my brain got frazzled and I couldn’t tell whether it was giving me incoherent results or if I was just implementing them incorrectly.
This looks suspiciously similar to a 2nd order low pass filter with some quirks I can’t say anything about.
Anyways, as a suggestion:
If you can do without the overshoot (= resonance), just use exponential smoothing (= 1st order low pass), which is more efficient too, and will probably suffice for what you want.
ie: current = current + (target - current) * approachFactor
with 0 < approachFactor < 1
cheap, easy to implement inline, less state to store, better than linear interpolation in many cases, and it works great for many kinds of modulation. note that “target” may never be reached, or it may be stuck very close to “target” because of floating point precision. you may also have to normalize “approachFactor” according to your update rate (it corresponds to the cutoff frequency of the filter).
I managed to solve this without the use of any complicated mathematics, and (to my surprise) without even using modulo. Here’s my formula. (This is Javascript, but it should be easily portable for any language).