I spent about 8 hours debugging this today, and what I discovered is that the part that causes the sample rate to be incorrectly reported to an AU plugin (in the JUCE AudioPluginHost) is in juce_AudioUnitPluginFormat.mm class AudioUnitPluginInstance:
// AudioProcessor methods:
void prepareToPlay (double newSampleRate, int estimatedSamplesPerBlock) override
{
if (audioUnit != nullptr)
{
releaseResources();
if (isMidiEffectPlugin)
{
outputBufferList.add (new AUBuffer (1));
}
else
{
for (int dir = 0; dir < 2; ++dir)
{
const bool isInput = (dir == 0);
const AudioUnitScope scope = isInput ? kAudioUnitScope_Input : kAudioUnitScope_Output;
const int n = getBusCount (isInput);
for (int i = 0; i < n; ++i)
{
Float64 sampleRate;
[snip...]
The part at “if (isMidiEffectPlugin)” is key. Because the whole “else” part underneath it (not all shown) is what sets the correct starting sampleRate. And that is what is not bothered to be provided to MIDI plugins.
So, I found that I could fix that in the JUCE AudioPluginHost by copying parts of the bottom section into the top section - and it works. But so what? That fixes if for the AudioPluginHost only; doesn’t do anything for Logic or any of the other hosts reportedly having this issue.
Therefore, I came up with the following fix, which I share with you in the hope it helps you, or you have some comments on the viability of it.
Basically, we can “guess” the correct incoming sampleRate by performing time calculations on how often processBlock() is called, compared to Time::getMillisecondCounterHiRes().
So the following code, whenever the sample rate is changed or upon startup, averages a few calls to processBlock and determines the sampleRate.
The reason there are delays and an average rather than just doing it immediately with one call, is that my testing has shown that changing the sample rate, or the buffer size, can cause processBlock to take a bit of time to stabilize. If you have any improvements to this idea, let me know.
So the current idea is to delay for 1 second after receiving a call to prepareToPlay(), allow the system to stabilize, then sample the times and make a calculation. Here we go:
Add these variables to your AudioProcessor:
private:
bool checkSampleRate = false; // flags a new sample rate test
double csrTestStartTime; // time at which the test was flagged
double csrDelayTime; // time to delay in ms before beginning the calculations
double csrStartTime; // start time of the calculations
int csrCounter; // allow a number of calls to be collected
int csrNumSamples; // accumlate block sizes between calls
Add this in your AudioProcessor’s prepareToPlay:
void PluginProcessor::prepareToPlay (double sampleRate, int samplesPerBlock)
{
ignoreUnused (samplesPerBlock);
mySampleRate = sampleRate; // store what they report, anyway
// if already in progress, skip it
if (!checkSampleRate){
checkSampleRate = true; // flag a start to the CheckSampleRate (csr) test
csrTestStartTime = Time::getMillisecondCounterHiRes(); // store the test start time
csrDelayTime = 1000.0; // ms delay before starting, to allow the new rate to stabilize
csrCounter = 0; // counter so we can sample a few blocks and average together
}
}
Add this near the head of your processBlock:
void PluginProcessor::processBlock (AudioBuffer<float>& buffer, MidiBuffer& midi)
{
const int numBlocksToAverage = 5;
if (checkSampleRate)
{
// check to see if the delay time has passed since the checkSampleRate
// was flagged, because tests have shown that changing the buffer size
// can sometimes take a bit of time to stabilize for these calculations
double elapsedTime = Time::getMillisecondCounterHiRes() - csrTestStartTime;
if (elapsedTime > csrDelayTime)
{
if (csrCounter == 0) // first one
{
csrStartTime = Time::getMillisecondCounterHiRes(); // store the start time
csrNumSamples = buffer.getNumSamples(); // store the buffer size
csrCounter++;
}
else if (csrCounter < numBlocksToAverage){
// accumulate 'n'' blocks worth of samples (in case it changes)
// meanwhile we are accumulating time
csrNumSamples += buffer.getNumSamples();
csrCounter++;
}
else if (csrCounter == numBlocksToAverage) // time to take the calculation
{
// take the average of 'n' blocks
elapsedTime = Time::getMillisecondCounterHiRes() - csrStartTime;
double sampleSizeMs = (elapsedTime / numBlocksToAverage) / (csrNumSamples / numBlocksToAverage);
checkSampleRate = false; // done with the test
// 1 second is 1000 ms; so the time duration of a single
// sample is 1000/sampleRate - therefore:
// at 96k, a single sample is 1000/96000 = .0104166 ms
// at 48k, a single sample is 1000/48000 = .0208333 ms
// at 44.1, a single sample is 1000/44100 = .0226757 ms
// at 22.05,a single sample is 1000/22050 = .0453514 ms
// now we can set the real sampleRate
if (sampleSizeMs < 0.015) // it's 96000
mySampleRate = 96000;
else if (sampleSizeMs < 0.0215) // it's 48000
mySampleRate = 48000;
else if (sampleSizeMs < 0.0250) // it's 44100
mySampleRate = 44100;
else if (sampleSizeMs < 0.0500) // it's 22050
mySampleRate = 22050;
else
{
jassertfalse;
mySampleRate = 44100; // default
}
}
}
}
}
That’s it. Now you can change sampling rates, buffer sizes, etc. and the plugin will always know what the “real” sampling rate is - albeit with a slight delay.
I welcome any commentary and improvements on this idea, but for now, it works!