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.
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!
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.
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..
}
}
@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.