BR: CoreAudio - audioDeviceIOCallback no longer called after sample rate change

Hi, it seems that if sample rate is changed without changing buffer size at the same time, then audioDeviceIOCallbacks stop. If buffer size is changed subsequently, then callbacks resume.

You can reproduce this in the AudioSettingsDemo as follows:

  • select a device and click Test to confirm audio is working
  • change the sample rate
  • click Test and there will be no audio
  • change buffer size
  • click Test and audio is working again

I’ve done a bit of digging through the last couple of month’s of changes to juce_AudioDeviceManager.cpp and juce_mac_CoreAudio.cpp but was unable to find the culprit.

Ooh - I see a little slice of cake next to my name.

image

Apparently I first joined this forum 11 years ago today. My how time flies!

1 Like

Are you seeing this on the very latest develop branch?

What platform causes the issue? I just tested an Intel and an Arm mac without problems - I’ll try iOS shortly, but it would be helpful if you could confirm whether I should be attempting to repro the issue on macOS or only on iOS.

I’m still struggling a bit to repro this. On an iPad running iOS 15.2, I can only pick a sample rate of 48KHz when the microphone and speakers are both enabled. When I disable the microphone I’m able to choose from more sample rates, and the ‘Test’ button produces sound every time I change the sample rate.

If you’re not running the very latest develop please try updating and see whether the problem is still present.

Sorry Reuben, its occurring on the current develop tip, with a 9 year old Intel iMac running 10.15.7. The issue was originally reported to me by a user with a MacBook (platform unspecified).

I’ve just done a bit more testing and found the issue is only occurring for me with a multi output composite audio device created in the Audio MIDI Setup app. I’ll check with my user to see if they were also using a composite device (I had assumed not because they usually use my app to test their own device drivers).

Apologies again for my lack of thoroughness, and the consequent sub-optimal use of your time.

I can confirm this is an issue also on windows 11. Exactly as Andrew describes. Furthermore, I dived down into my collection of old realase builds and got the same outcome from an over one year old build.

Develop branch downloaded as zip as of this hour, DemoRunner VS 2022, added juce_ASIO 1,
audio device Focusrite USB ASIO. iasiodrv.h is from 2017

Interesting - I’m not seeing that on Windows 10, but while double checking that I noticed that ASIO devices seem to hang more often than they used to when changing sample rate.

Some more findings:

  1. In release build I get this error message, probably from the OS, every time I change the sample rate
Error 7 in function StorageSystem::Registry::CNativeValue::Write: Write access denied for Registry value '{B598FAEC-CFE1-4819-B42D-C19440E62F4F}'
  1. If I click the Control Panel button of the audioDeviceManager and change the sample rate from there, everything works fine, it even updates the sample rate combo of the device manager in a second.

Same results on windows 10 for me, i.e error when changing sample rate

Thanks, I can see this too. I’ll investigate a bit and try to find out what’s going on.

I’ll do a bit of testing with ASIO and see whether I can reproduce this problem too.

I’ve found the cause of the issue for CoreAudio devices, and I’ll try to get the fix out shortly.

The problem I found was in the CoreAudio wrapper itself, so the ASIO issue is likely to be unrelated, even if the symptoms are similar.

2 Likes

I’ve done some digging.

When changing the sample rate via the AudioDeviceSelecorComponent combo,
this function in AudioDeviceManager is called repeatedly > 10 times, for reasons unkown to me…

String AudioDeviceManager::setAudioDeviceSetup (const AudioDeviceSetup& newSetup,
                                                bool treatAsChosenDevice)
{
    jassert (&newSetup != &currentSetup);    // this will have no effect

    if (newSetup != currentSetup)
        sendChangeMessage();
    else if (currentAudioDevice != nullptr)
        return {};

    stopDevice();

   <more code...>

Only in the first call newSetup differs from currentSetup and subsequently it’s only then
any real action is taken.

If you in the debugger, manually let also the second call pass the newSetup != currentSetup test, the change of sample rate will work as intended without needing to also change the buffer size.

You could modify the code further down in the function to let stopDevice() and open() called being twice:

<...other code...>

    <-- my code -->
    bool sampleRateChanged = newSetup.sampleRate != currentSetup.sampleRate && newSetup.sampleRate > 0.0 && currentDeviceType == "ASIO";
   <-- end my code -->

    currentSetup = newSetup;

    if (! currentSetup.useDefaultInputChannels)    numInputChansNeeded  = currentSetup.inputChannels.countNumberOfSetBits();
    if (! currentSetup.useDefaultOutputChannels)   numOutputChansNeeded = currentSetup.outputChannels.countNumberOfSetBits();

    updateSetupChannels (currentSetup, numInputChansNeeded, numOutputChansNeeded);

    if (currentSetup.inputChannels.isZero() && currentSetup.outputChannels.isZero())
    {
        if (treatAsChosenDevice)
            updateXml();

        return {};
    }

    currentSetup.sampleRate = chooseBestSampleRate (currentSetup.sampleRate);
    currentSetup.bufferSize = chooseBestBufferSize (currentSetup.bufferSize);

    error = currentAudioDevice->open (currentSetup.inputChannels,
                                      currentSetup.outputChannels,
                                      currentSetup.sampleRate,
                                      currentSetup.bufferSize);

<-- my code -->

    if (sampleRateChanged)
    {
       stopDevice();

       Thread::sleep(500);

       error = currentAudioDevice->open(currentSetup.inputChannels,
          currentSetup.outputChannels,
          currentSetup.sampleRate,
          currentSetup.bufferSize);
    }
<-- end my code -->

This will make the problem go away. But I’m not all to happy with this code, especially the needed
500ms sleep, (200ms won’t do), and it might possibly create problems for other devices.

Anyway, it might help you to find a better solution…

I’ve tried reproducing the ASIO problem now. My testing machine has two ASIO device drivers:

  • built-in Realtek Audio (speakers)
  • Universal Audio Thunderbolt driver (Apollo Solo)

I wasn’t able to reproduce the issue you mentioned, specifically that the audio device callback is no longer called after changing the sample rate. I was still able to play the test sound after changing the sample rate for both devices.

However, I did find a couple of other problems:

  • For the Realtek device, switching to the very lowest sample rate (44.1KHz) sometimes caused the test tone to be garbled. Resetting the device seemed to fix the problem.
  • For the Universal Audio device, changing the buffer size from the control panel issued a restart request to the device, but then failed to create new buffers afterwards. The problem sounds very similar to the one mentioned here.

For all of these issues, it’s not clear whether the problem is in JUCE or in the device driver.

Do you have any other ASIO device drivers available for testing? Do they all behave in the same way? If not, I’d recommend contacting Focusrite and asking whether they can provide any insights.

I tested a bit in REAPER too, and that was able to play audio successfully when I tried to reconstruct the same scenarios - however, it seems like the configuration flow is substantially different there. As far as I can tell, opening the “Audio device settings” panel closes the audio device completely, and the device is not reopened until the panel is closed. Although we could completely unload and reload the driver on each change (essentially running the same steps as the “Reset Device” button), I don’t think this is a good idea, as it would be very slow. In addition, the ‘state machine’ diagram in the ASIO SDK docs implies that it should be possible for devices to transition between states without requiring the driver to be reloaded each time.

I think this is the right place to add other behavior I have observed recently on macOS - certain edge cases in submitting a new AudioDeviceSetup result in unexpected behaviors such as a returned sampleRate of 0 samples. For example:

  1. Initialize a starting full duplex setup using a multichannel device capable of both input and output (I used a MOTU UltraLite mk3). It begins working as intended with (IIRC) 10 inputs and (definitely) 14 outputs available. In reality, only the input is actually being used in the app (the mic in on ch.1)
  2. Intend to switch to a new input device, which has only (2) inputs available and no outputs (I used a Logitech C922 Pro webcam). On macOS, these two audio devices also have a mutually exclusive set of available sample rates - the Logitech (and also many bluetooth devices) only support 16kHz while the Pro Audio card only has 44.1kHz and up. This intent is specified by getting the current audioSetup, then modifying the inputDevice field, and submitting the new setup.
  3. Internally to juce_mac_CoreAudio.cpp, it seems to try to initialize an “aggregate” device that uses the new webcam device as input, and the old MOTU soundcard as output. There is a loop where the first iteration succeeds in initializing the input device, but fails at initializing the output device, (now treated as a device with 0 inputs and 14 outputs), and ultimately the method setAudioDeviceSetup() fails reporting an inability to change the sample rate.

NOTE: in an input-only scenario, the solution to the above is very hard to guess via the available API - one needs to set the AudioDeviceSetup& newSetup’s .outputDevice string to an empty string.

Now that I am adding output functionality to my library, I can see that the above workaround is not going to be enough. I need some way of telling the user that a certain combination of input and output devices is not possible, or else silently run the devices at differing sample rates, using resamplers internally as needed (which makes me worry about synchronization issues…)

I wonder if it might be possible to come up with an API wrapper for AudioDeviceSetup changes that makes parameterization of new AudioDeviceSetups more robust and explicit, to better capture the intention of the API user.

Thanks for looking into both the Apple and Windows sides of things all at once. I’d like to try adding in my observations on the Windows side later this week once I can take a look in more detail. The one outstanding issue we’ve faced on the Windows side is that some of these older Realtek devices don’t seem to even be recognized via WASAPI until they have been set as the default device in the Windows sound panel once. However, these devices can be recognized and made available using DirectSound regardless.

Needless to say this is very hard to investigate because once you’ve made a device the default in the Sound Control Panel, you can no longer reproduce the issue.

A fix for the CoreAudio issue has been merged:

1 Like

Thanks Reuben - that fixes CoreAudio for me (though I am still awaiting confirmation from my user).

BTW, that was a subtle bug, cleverly disguised by the funky use of std::exchange. Nice pickup!

Well it’s no big concern of mine that Focusrite needs a reset/restart after a change of sample rate. I was just triggered by Andrews original post to check this out myself.

Quite possibly a problem that’s gone under the radar for a long time. At least for me, I tend not to fiddle around much with the sample rate, and now, knowing what’s happening, it’s just an extra button click. Manageable, compared to other things in life…