My AU plugin on Mac OSX in AudioPluginHost receives wrong sample rate!

I’m using the JUCE AudioPluginHost 5.4.4 on Mac OS X Mojave to test plugins I am working on. In the Audio Settings dialog of the AudioPluginHost, it is set to 48000, and that is the only choice available:

But the AU version of the plugin I am working on reports 44100 in AudioProcessor::prepareToPlay():

Even calling AudioProcessor::getSampleRate() inside of processBlock() returns the wrong rate of 44100.

I know it’s wrong because I’m doing some time-based calculations based on the sample rate, and my calculations are drifting from the actual real time by an amount equal to the difference between 48000 and 44100. So it is running at 48000, but referencing the incorrectly reported value of 44100.

The VST3 version of the same plugin correctly reports 48000 in prepareToPlay.

But I compiled and ran the JUCE AudioPluginDemo, and the AU version correctly reports the sample rate as 48000:

If I select the built-in Line Output as the input, I have 3 different sample rates to choose from: 44100, 48000, and 96000. But regardless of which I select, my plugin receives 44100.

Please help, if you can - I’m about at my wit’s end with this…

Ahh… I seem to be tracking it down. This line in juce_AU_Wrapper.mm:

double getSampleRate()   { return AudioUnitHelpers::getBusCount (juceFilter.get(), false) > 0 
? GetOutput (0)->GetStreamFormat().mSampleRate : 44100.0; }

It seems I don’t have busses set up or something. New to me. This is a midi processing plugin…

See here AU/VST plugin does not recognize changed sample rate of the host

Thanks for the link. It shows there are other people having the same problem, but no solution.

I added an output bus to my MIDI plugin (already had an input bus). Useless for a MIDI plugin, but OK, let’s just see - no, it does not fix the problem. It does get it past that line in the code where 0 output buses set it to 44100. But then it is apparently querying the host and getting the wrong sample rate.

Yes I think the conclusion is, it’s a problem for AU MIDI plugins, in other words I don’t believe there is a solution. Unless you have evidence of a MIDI plugin that is capable of doing this?

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!

I am quite weary about this solution, since it assumes realtime operation. Otherwise your comparison against the wall clock (Time::getMillisecondCounterHiRes()) will lead to hilarious results (up to +inf samples block size, when the spent time in an offline render moves towards zero)

There are workarounds in other professional products as well, where you have to push play once, before anything works though, but that’s not really satisfying IMHO…

I don’t have an alternative solution though…

Well, I am new to trying to develop a plugin, so there are probably things I haven’t thought of in regards to this. At least it allows me to proceed for the moment… I can’t believe how poorly everyone deals with MIDI plugins. They are like the poor bastard children that no one cares for.