Hi everyone! I’m working on building a pitch shifting plugin that uses an ESOLA shifting algorithm, which is implemented in a custom rewrite of the Synthesiser class to allow polyphony. Within each individual voice, the esola algorithm itself requires the detection of the input pitch (fundamental frequency) and the detection of the signal’s epoch locations.
I already have my detectPitch() and extractEpochSampleIndices() functions written – my question is this – would it be wiser to put these two functions in the Synthesiser class’s renderVoices(), or put them at the top level within my processBlock() before calling the Synthesiser’s renderNextBlock() ?
to clarify…
Option 1:
void AudioProcessor::processBlock (juce::AudioBuffer<float>& buffer, juce::MidiBuffer& midiMessages)
{
synth.renderNextBlock(buffer, 0, buffer.getNumSamples(), wetBuffer, inputMidi);
}
and then inside renderNextBlock(), the Synthesiser class breaks the input buffer down into smaller chunks between midiMessages and calls this function on each smaller chunk:
void Synthesiser::renderVoices (AudioBuffer<float>& inputAudio, const int startSample, const int numSamples, AudioBuffer<float>& outputBuffer)
{
epochIndices = extractEpochSampleIndices(inputAudio, startSample, numSamples, sampleRate);
currentInputFreq = findPitch(inputAudio, startSample, numSamples, sampleRate);
for (auto* voice : voices)
{
voice->updateInputFreq(currentInputFreq);
voice->renderNextBlock (inputAudio, startSample, numSamples, outputBuffer, epochIndices);
}
}
Option 2:
void AudioProcessor::processBlock (juce::AudioBuffer<float>& buffer, juce::MidiBuffer& midiMessages)
{
// these two would be custom functions that take the pitch & epoch data and propogate to each synth voice:
synth.updateInputPitch(findPitch(inputAudio, startSample, numSamples, sampleRate));
synth.updateEpochs(extractEpochSampleIndices(inputAudio, startSample, numSamples, sampleRate));
synth.renderNextBlock(buffer, 0, buffer.getNumSamples(), wetBuffer, inputMidi);
}
and then the renderVoices would simply be:
void Synthesiser::renderVoices (AudioBuffer<float>& inputAudio, const int startSample, const int numSamples, AudioBuffer<float>& outputBuffer)
{
for (auto* voice : voices)
voice->renderNextBlock (inputAudio, startSample, numSamples, outputBuffer, epochIndices);
}
Is there any practical difference between these two approaches, in terms of performance or stability?
The only potential concern I have is, because the Synthesiser class in its renderNextBlock() breaks the input buffer into small chunks in between midi messages, if I have my detechPitch() and findEpochs() in the renderVoices(), they may get passed too short of a chunk to be able to correctly detect pitch/epochs… But I could just use setMinimumRenderingSubdivisionSize() to set a large enough minimum block size…
I was thinking that maybe putting these functions in renderVoices() would allow for a greater level of synchronicity, but perhaps that’s not the case. There is also the possibility that the input pitch varies over the course of the input buffer, so detecting pitch in smaller chunks may be desirable…
Sorry for the long post. Thanks for reading!