Bug fix in iOSAudioIODevice::Pimpl::tryBufferSize for iOS 18

Thank you for trying it out. Can you share what devices you used (iOS hardware, iOS version, external sound card) when seeing these issues? Are you able to repro the problem using just the DemoRunner?

Ideally we should be able to reproduce these issues before taking another attempt at a workaround.

Hi,

I’ve tested it with an iPad 9th gen, iOS 18.0 and a Behringer 404HD / Audient EVO4. I’ve not tested with the Demo Runner, but my app is basically a standard JUCE standalone app with a audioDeviceSelectorComponent that is the same as the JUCE stock one with just a different LookAndFeel and element layout. The only modification I’ve made to the logic inside that class is the one I’ve posted above, but in the meantime I’ve removed it and the issue seems unaffected.

In my case, also using a 9th gen iPad with its internal sound, I’m getting issues when another audio application is running. For example if iOS “Voice Memos” is currently recording, I can’t change anymore the sample rate in my app and the buffer size receives a strange value, such as 1104 samples.

(this is after applying the “if (@available(iOS 18, *))” patch).

Hi, any news?

There’s a patch underway, currently awaiting review. I expect it to be released at some point next week unless we discover any new issues.

1 Like

There are now some patches on the develop branch that should help with this issue.

We kept the initially-suggested change, as it seems like a good idea to request the exact buffer size we want:

On iOS 18, we now ensure the audio device is deactivated when requesting a particular buffer size, and then activate the device and wait for it to deliver an audio callback before requesting the real buffer size. This could take a long time when querying a lot of buffer sizes, but it’s the most consistent technique we found to get the correct results.

Because querying lots of buffer sizes could take a long time, we now hard-code the list of buffer sizes on iOS 18. The list of supported buffer sizes should be treated more like a list of suggestions. It’s possible that applying a very large or very small size could fail, but the system should fall back to a sensible supported size in this case. It seems that other iOS audio apps (Logic, AUM, Loopy Pro) take a similar approach, though it’s difficult to say for sure.

This fix isn’t perfect - we still managed to trip it up by changing the buffer size lots of times in a row, but it’s definitely an improvement. The root cause of the issue is an undocumented change in the contract of AVAudioSession on iOS 18. We’ve filed an issue with Apple and will update the solution if we receive new guidance on the approach we should be using to query audio device capabilities.

1 Like

Tested yesterday, it works! Yes, it is a little bit slower, but all in all it’s ok, thank you @reuk, awesome works and support as always!

It seems to work now (JUCE 8.0.3) on iOS18, but I get the impression that iOS 17.7 and earlier is now broken. If upon starting an app, I request info from the current AudioIODevice (in a custom stand-alone wrapper), on IOS 17.7 I’m getting inconsistent results.

    const double        currentSampleRate       = currentDevice->getCurrentSampleRate() > 0 ? currentDevice->getCurrentSampleRate() : 48000;
    const Array<int>    availableBufferSizes    = currentDevice->getAvailableBufferSizes();
    const int           defaultBufferSize       = currentDevice->getDefaultBufferSize();
    const int           currentBufferSize       = currentDevice->getCurrentBufferSizeSamples();

What I’m getting is a sample rate of 48 kHz, a default buffer size of 256, a current buffer size of 256, but the array of availableBufferSizes is off, containing the following sizes: 59, 118, 236, 472, etc.

Interestingly the ‘currentBufferSize’ and ‘defaultBufferSize’ (both 256) aren’t even part of the array of ‘availableBufferSizes’?

Once I select one of the odd buffer sizes (let’s say 236), and request buffer sizes again, they now come back as expected values (64, 128, 256, 512 etc).

Has anyone seen this odd behaviour? It can be reproduced using the JUCE demo runner app. Simply deploy it on an IOS17.7 device, and go to the audio settings section and click on the buffer size drop down menu. You’ll initially see odd buffer sizes, which will flip back to expected numbers (powers of two) as soon as you select a buffer size.

I also went back to deploying on an old iOS 12 device and I’m getting similar issues.

Sorry about that. I’m able to reproduce the issue, and I have a potential fix on the way. I’ll update this thread once it’s released.

1 Like

Thanks for your patience, the fix is now available:

Please try it out and let us know if you encounter further issues.

1 Like

Thanks @reuk for the quick turnaround; tested this on iOS17.7 and iOS18.0.1 and setting buffer sizes now works as expected.

Hi there,
I’m now experiencing a crash when using the JUCE_IOS_AUDIO_EXPLICIT_SAMPLERATES macro on iOS 17 and earlier.

I have an app where this macro is set to use 44100, 48000, 88200 and 96000 values.
When I select a value that is not supported by the current peripheral (e.g. 88200), Core Audio will correctly refuse to use it and JUCE will read the new sample rate and buffer size from the session. In my case, I can see that if I was previously running at 44100 with a buffer size set to 256, Core Audio will switch to 48000 with a buffer size of 512 samples.
However, on the client side, the device passed by my audioDeviceAboutToStart implementation will report a buffer size of 256 samples.
Then, at the first audio callback call, my app will crash because the actual size in the callback will be set to 512.

In commit 70c9c5b by @attila there’s a comment that talks about how the Core Audio documentation suggests that the buffer size should be set when the session is inactive. That’s correct, I actually do the same in other non-JUCE based implementations:

        // According to the apple docs, it's best to set preferred sample rates and block sizes
        // while the device is inactive, and then to query the real values after activation.
        // Unfortunately, on iOS 18.0, the real block size isn't immediately available after
        // a call to setActive, so we also need to wait for the first audio callback.
        // This will be slow!
        // https://developer.apple.com/library/archive/qa/qa1631/_index.html
        if (@available (ios 18, *))
            setAudioSessionActive (false);

        JUCE_NSERROR_CHECK ([session setPreferredIOBufferDuration: bufferDuration error: &error]);

        if (@available (ios 18, *))
            setAudioSessionActive (true);

However, in that commit, the session is deactivated only on iOS 18.
Removing the last 2 “are we on iOS 18?” checks fixes the crash for me.

Thanks for the detailed write-up. Yesterday I proposed removing those @available checks as a fix for another issue here - unfortunately, I wasn’t able to reproduce that issue, so I don’t know whether it’s an effective fix in that case.

I’ll have a go at reproducing the issue with explicit sample rates.

I’m able to reproduce the issue you described, and I can confirm that removing the availability checks resolves the issue.

Great :partying_face:
Is there an ETA on when the change could appear on github?

The change is available now!

Please try it out and let us know if you encounter any new issues.

Now that was fast… thanks! :metal:

Hi @reuk,

It was nice catching up with you at ADC :slight_smile:

I was able to do some thorough testing on multiple devices and I can say that the fix works happily on most of them.
One particular device though is still causing some issues when using the JUCE_IOS_AUDIO_EXPLICIT_SAMPLERATES macro.

The device in question is a 5th Generation iPad Air running iOS 17.

What’s peculiar about this device is the fact that it doesn’t seem to be able to run at 44100 Hz and its first available rate is 48000 Hz.
As stated in my previous post, I’m setting the macro with 44100, 48000, 88200 and 96000.

When the application starts and JUCE opens the peripheral, the query of available sample rates is bypassed, so the sampleRate member variable
in the iOSAudioIODevice::Pimpl struct stays at 44100.

Unfortunately, that variable is then used immediately after when updateAvailableBufferSizes() is called.

This leads to a miscalculation when setting the bufferDuration in the tryBufferSize function, where currentSampleRate should be 48000 but instead we have 44100.

I believe we should simply update the sampleRate member variable in the updateAvailableSampleRates function also when the
JUCE_IOS_AUDIO_EXPLICIT_SAMPLERATES is set. JUCE is already doing it when the macro is not set:

// Important: the supported audio sample rates change on the iPhone 6S
    // depending on whether the headphones are plugged in or not!
    void updateAvailableSampleRates()
    {
        if (iOSExplicitSampleRates.size() != 0)
        {
            availableSampleRates = Array<double> (iOSExplicitSampleRates);
            sampleRate = trySampleRate (sampleRate); // This is the fix
            return;
        }
        ...
    }

This change fixes the problem for me.
Please let me know what you think.

Cheers!

@reuk It seems that it doesn’t work yet on Apple Silicon devices.
I tested with a real “iPad Air 13-inch M2” (iOS 17.6). On this device strange buffersizes are reported. I could get the same results with the AUv3SynthPlugin using these extra preprocessor definitions and the simulator (same device type, but iOS 17.5).

JUCE_IOS_AUDIO_EXPLICIT_SAMPLERATES=44100\,48000\,96000
JUCE_IOS_AUDIO_LOGGING

juce log:

JUCE v8.0.3
Creating iOS audio device
Updating hardware info
Available buffer sizes: 59 118 236 472 944 1888 3763
Buffer size after detecting available buffer sizes: 470
Input channel configuration: {Number of hardware channels: 2, Hardware channel names: "Left" "Right", Are channels available: yes, Active channel indices:, Inactive channel indices: 0 1}
Output channel configuration: {Number of hardware channels: 2, Hardware channel names: "Left" "Right", Are channels available: yes, Active channel indices:, Inactive channel indices: 0 1}
Opening audio device: inputChannelsWanted: 0, outputChannelsWanted: 11, targetSampleRate: 44100, targetBufferSize: 512
Input channel configuration: {Number of hardware channels: 2, Hardware channel names: "Left" "Right", Are channels available: yes, Active channel indices:, Inactive channel indices: 0 1}
Output channel configuration: {Number of hardware channels: 2, Hardware channel names: "Left" "Right", Are channels available: yes, Active channel indices: 0 1, Inactive channel indices:}
Updating hardware info
Available buffer sizes: 59 118 236 472 944 1888 3763
Buffer size after detecting available buffer sizes: 470
Setting target sample rate: 44100
Actual sample rate: 44100
Setting target buffer size: 512
Actual buffer size: 512
Creating the audio unit
Internal buffer size: 4096

Since I work on a M1 machine, I get the same log with the iOS simulator also for older devices.

Could you check this? I want to file an update for the iOS 18 compatibility problem discussed above, but I would like to integrate a fix for this, too.

Unfortunately I don’t have an M-series iOS device that I can test. Please could you try adding the fix suggested by @masshacker above, and see whether that resolves the issue for you?

I’m hoping to spend some time on this issue later on today, and it would be helpful to know whether the suggested change works on several different devices.