So I have a pretty robust and fast pitch tracker I've been working on based on this thesis. I want to use it for real-time pitch shifting. However, one of the simplest algorithms for formant-preserving pitch shifting (PSOLA) doesn't seem to have a standard easy to find real-time implementation. As such, I bodged the following:
#define BUFFER_SIZE (1 << 10)
#define STEP_SIZE (1 << 8)
#define SAMPLE_RATE 44100
#define NOISE_THRESHOLD 0.95
#define NOISE_PERIOD SAMPLE_RATE * 0.005
void tdpsola() {
if (clarity < NOISE_THRESHOLD) {
while (acc < STEP_SIZE) {
for (t = -NOISE_PERIOD; t < NOISE_PERIOD; t++) {
output_buf[BUFFER_SIZE - STEP_SIZE + (int)acc + t] +=
sinf(M_PI * (t + NOISE_PERIOD) / (2 * NOISE_PERIOD - 1)) *
sinf(M_PI * (t + NOISE_PERIOD) / (2 * NOISE_PERIOD - 1)) *
input_buf[BUFFER_SIZE - STEP_SIZE + (int)(acc - NOISE_PERIOD) +
t];
}
acc += NOISE_PERIOD;
}
} else {
while (acc < STEP_SIZE) {
int t, epoch;
float peak = 0, val;
int upper =
acc + 2 * period < STEP_SIZE ? acc + period : STEP_SIZE - period;
int lower = upper - period;
for (epoch = t = lower; t < upper; t++) {
val = fabsf(input_buf[BUFFER_SIZE - STEP_SIZE + t]);
if (val > peak) {
peak = val;
epoch = t;
}
}
for (t = -period; t < period; t++) {
output_buf[BUFFER_SIZE - STEP_SIZE + (int)acc + t] +=
sinf(M_PI * (t + period) / (2 * period - 1)) *
sinf(M_PI * (t + period) / (2 * period - 1)) *
input_buf[BUFFER_SIZE - STEP_SIZE + epoch + t];
}
acc += target;
}
}
acc -= STEP_SIZE;
}
This runs every step, which starts by moving back both input_buf
and output_buf
by STEP_SIZE
, loading STEP_SIZE
samples at the end of input_buf
, detecting the pitch and saving it to period
, as well as taking note of the confidence of the pitch measure in clarity
, setting a target
, running tdpsola
, and finally outputting the samples from output_buf[BUFFER_SIZE - STEP_SIZE]
to output_buf[BUFFER_SIZE-1]
.
The choice of pitch period window to use for the overlap-add part was done as to minimize latency, as the last possible peak for which we could derive a full pitch window.
Needless to say, it "works". However, it sounds absolutely horrible, which I suspect is due to aliasing. There's a harsh bitcrusher-like distortion which varies highly with STEP_SIZE
. Any advice on improving this implementation?
To give a bit of context, here's a flute sample. It's not a voice as PSOLA is designed for, but it's a pulse train instrument nonetheless. The program is setup to pitch shift to the nearest A440 equal-tempered note. It works but the artifacts are very strong.