ASIO issue - buffer sizes not being correctly updated

Whilst providing feedback on my RTL Utility app, RME asked if I could improve the handling of buffer size changes. They also mentioned that their ASIO drivers only allow the buffer size to be changed in their control panel and only show the current buffer size in the list of available buffer sizes. Apparently this is according to the ASIO spec and has been historically confirmed by Steinberg.

Repro & current behaviour (with RME ASIO devices):

  • Instantiate an AudioDeviceSelectorComponent
  • User opens the ASIO device control panel (either via the button provided by AudioDeviceSelectorComponent or directly)
  • User chooses a different buffer size
  • juce::ASIOAudioIODevice responds to the kAsioResetRequest and changes to the new buffer size
  • However, AudioDeviceSelectorComponent displays a blank value in the Audio Buffer Size combo box

This is because the set of buffer sizes has not been properly updated, and the new buffer size is not a member of the previous set. As can be seen from the code below, refreshBufferSizes() updates the min, max, preferred & granularity parameters - but it does not update the bufferSizes member.

modules\juce_audio_devices\native\juce_win32_ASIO.cpp, lines 822-825:

	long refreshBufferSizes()
	{
		return asioObject->getBufferSize (&minBufferSize, &maxBufferSize, &preferredBufferSize, &bufferGranularity);
	}

Here’s a patch to fix this issue:

long refreshBufferSizes()
{
    const auto err = asioObject->getBufferSize (&minBufferSize, &maxBufferSize, &preferredBufferSize, &bufferGranularity);
    if (err == ASE_OK)
    {
        bufferSizes.clear();
        addBufferSizes(minBufferSize, maxBufferSize, preferredBufferSize, bufferGranularity);
    }
    return err;
}

This seems to work flawlessly on my system with an RME Fireface 800.

1 Like

That seems like a sensible addition, thanks. We’ll get that added to the develop branch.

1 Like

Thanks for adding this @ed95, but now I’ve found another one. Again seen with RME devices, when you change to a double or quad sample rate (e.g. 192K) then the buffer size needs to change. Similar to the above issue, the new buffer size would actually take hold, but the device manager goes blank.

I’ve tracked this down to the fact that the buffer sizes are read before setSampleRate() is called in the open() method. Simply moving a couple of lines to after the setSampleRate() call fixes it up. Here’s the diff (thought the dark theme makes it hard to read!).

diff --git a/modules/juce_audio_devices/native/juce_win32_ASIO.cpp b/modules/juce_audio_devices/native/juce_win32_ASIO.cpp
index cb8f6c5db..336224384 100644
--- a/modules/juce_audio_devices/native/juce_win32_ASIO.cpp
+++ b/modules/juce_audio_devices/native/juce_win32_ASIO.cpp
@@ -415,11 +415,8 @@ public:
         auto err = asioObject->getChannels (&totalNumInputChans, &totalNumOutputChans);
         jassert (err == ASE_OK);

-        bufferSizeSamples = readBufferSizes (bufferSizeSamples);
-
         auto sampleRate = sr;
         currentSampleRate = sampleRate;
-        currentBlockSizeSamples = bufferSizeSamples;
         currentChansOut.clear();
         currentChansIn.clear();

@@ -441,6 +438,8 @@ public:
         buffersCreated = false;

         setSampleRate (sampleRate);
+        bufferSizeSamples = readBufferSizes (bufferSizeSamples);^M
+        currentBlockSizeSamples = bufferSizeSamples;^M

         // (need to get this again in case a sample rate change affected the channel count)
         err = asioObject->getChannels (&totalNumInputChans, &totalNumOutputChans);

None of the buffer size variables are referenced by anything between their current location and the setSampleRate() call, so I think it is quite safe to move them. Works like a dream for me!

Great, thanks. I’ve added that and the max buffer size suggestion (Request - can we allow block sizes of up to 32768 for ASIO?) and they will be on the develop branch shortly.

Awesome, thanks again Ed!

Hi @ed95 - a customer has helped me find and fix another issue related to this. It turns out that MOTU M4 devices aren’t immediately reporting the correct buffer sizes back after a sample rate change - this leads to “ASIO: error: create buffers 2 - Invalid Parameter” and then the ASIO device is closed.

A short sleep in setSampleRate() at line 965 of juce_win32_ASIO.cpp fixes this:

                err = asioObject->setSampleRate (newRate);
            }

+           // Give a chance for devices to respond to new requests (e.g. buffer size queries) after
+           // changing sample rate (e.g. for MOTU M4)
+           Thread::sleep (10);

            if (err == 0)
                currentSampleRate = newRate;

Noting that similar sleeps are already used after the previous sample rate and clock source changes, it makes sense that we also sleep after the final sample rate change.

@ed95 bumping in case you missed this due to ADC…

Thanks for reporting. I’ve added this to develop here:

Thanks @ed95 - though doesn’t that mean we don’t do a sleep if there was a successful rate change at line 953?

I may need to revert back to the user who reported the issue and see if your fix works for them.

I should add that the log file shows neither of the log messages in the if clause at line 955 - and thus the setSampleRate call at line 953 must have been successful. So your placement of the sleep within the if clause will not solve this particular issue.

Ah, gotcha. I didn’t spot that the block would only be executed on the ASE_NoClock error. The extra logging statements are probably useful, so I’ll make the following change:

    void setSampleRate (double newRate)
    {
        if (currentSampleRate != newRate)
        {
            JUCE_ASIO_LOG ("rate change: " + String (currentSampleRate) + " to " + String (newRate));
            auto err = asioObject->setSampleRate (newRate);
            JUCE_ASIO_LOG_ERROR ("setSampleRate", err);
            Thread::sleep (10);
            
            if (err == ASE_NoClock && numClockSources > 0)
            {
                JUCE_ASIO_LOG ("trying to set a clock source..");
                Thread::sleep (10);
                err = asioObject->setClockSource (clocks[0].index);
                JUCE_ASIO_LOG_ERROR ("setClockSource2", err);
                Thread::sleep (10);
                err = asioObject->setSampleRate (newRate);
                JUCE_ASIO_LOG_ERROR ("setSampleRate", err);
                Thread::sleep (10);
            }

            if (err == 0)
                currentSampleRate = newRate;

            // on fail, ignore the attempt to change rate, and run with the current one..
        }
    }

This should be on develop shortly.

Cool, thanks!

@ed95 - I just noticed that the setClockSource() call now has two sleeps between it and the initial setSampleRate() call. The second of these is probably superfluous but I doubt it causes any issues.

This is on develop now:

1 Like

Thankyou!