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

Our iOS apps had horrible audio performance on iOS 18. We also noticed that we only saw one available buffer size (96) instead of the usual list from 128 to 4096 (on my iPad Mini, for example).

The cause of this is a bug in tryBufferSize(). We use JUCE 6 and 7, but I just looked at the JUCE 8 source, and it is still there as well.

setPreferredIOBufferDuration must be called with a buffer duration in seconds. The code was calculating the duration as:

((newBufferSize + 1) / currentSampleRate)

But it should be this:

(newBufferSize / currentSampleRate)

Apparently on iOS 18 this makes a difference. All calls to tryBufferSize() from updateAvailableBufferSizes() were only ever getting an end result of 96 samples. No error was reported by setPreferredIOBufferDuration either.

5 Likes

Thanks for reporting, I can repro the behaviour you’re seeing where only a single buffer size is reported as available. This seems specific to hardware devices running iOS 18. The iOS 18 simulator isn’t affected, and an older iPhone running iOS 16.4.1 also isn’t affected.

The proposed change looks reasonable. However, seeing as the + 1 was initially added back in 2016, I’m worried that removing it may break compatibility with some older devices running iOS 12+. Unfortunately I don’t have any such devices to hand right now - have you been able to test the change on any older hardware devices?

The change works on devices going back to iOS 15 for sure, but we just tracked down an old iPad running iOS 12, and the new code does not work, but the original code does. So this is a change that is iOS version specific. Considering that the old code works on iOS 17 and older, this could be a change for just iOS 18+ to be safe.

1 Like

Thanks for testing it out, that’s useful information. Checking the current OS version does sound like a good idea.

I just added

if (@available(iOS 18, *))
   bufferDuration = ...
else
   bufferDuration =  ...

to determine which calculation to use, and that works fine for us.

1 Like

Guys this is a massive bug, when will be released an update with this fix? Is there something we can do besides changing manually the JUCE source code (which is something I really would like to avoid in a production scenario!) ?

This snippet does not work for us, could you please confirm that this is the way to code it?

    int tryBufferSize (const double currentSampleRate, const int newBufferSize)
    {
        NSTimeInterval bufferDuration;
        
        if (@available(iOS 18, *))
            bufferDuration = currentSampleRate > 0 ? (NSTimeInterval) ((newBufferSize) / currentSampleRate) : 0.0;
        else
            bufferDuration = currentSampleRate > 0 ? (NSTimeInterval) ((newBufferSize + 1) / currentSampleRate) : 0.0;

        auto session = [AVAudioSession sharedInstance];
        JUCE_NSERROR_CHECK ([session setPreferredIOBufferDuration: bufferDuration
                                                            error: &error]);

        return getBufferSize (currentSampleRate);
    }

For the JUCE team: this bug is critical, we all have apps on the App Store that are unusable on iOS 18 because of this bug, I think this should be addressed with maximum priority

Sorry for not posting the entire method since it was just a minor change. Your code looks the same as our modified code.

1 Like

Unfortunately it does not work for us, there are also problems when switching sample rate.

So, before adding the above mod the buffer size drop down menu in our app was simply empty (usually shows only power of 2 buffers from 32 to 1024, to avoid unnecessary long menus when the soundcard supports lots of buffer sizes).

After adding that mod some buffer sizes appears in the combobox, but the behavior is totally unpredictable and often selecting a value from the menu does not change anything or makes the sound corrupted. Changing sample rate also affects the issue, making it even more difficult to understand and fix. That’s not all, sometimes when you change buffer size it changes the sample rate!

Of course the exact same app on iOS 17 works flawlessly.

Any updates from the JUCE team? Sorry to bump up the thread but this is a critical issue!

We have a fix in progress and we’ve already done some testing on iOS 18 and older devices.

I’m sharing the currently proposed fix.
ios18.patch (1.3 KB)

It’s likely to arrive on develop tomorrow, unless a regression is uncovered.

This looks like the code snippet I’ve uploaded before, and unfortunately I’ve tried it and it is not enough to fix the problem. As described before, this issue is affecting also the sample rate selection and seems to be unpredictable. I’ve tested it with a couple of very famous sound cards with the same results. The problem does not arise in the iOS simulator, only on real devices.

In “updateBufferSizeComboBox” inside my custom AudioDeviceSelectorComponent I’ve got this code, which is pretty the same as the original JUCE class

        for (auto bs : currentDevice->getAvailableBufferSizes())
        {
            if (bs <= 1024 && bs > 16)
                bufferSizeDropDown->addItem (String (bs) + " samples", bs);
        }

        bufferSizeDropDown->setSelectedId (currentDevice->getCurrentBufferSizeSamples(), dontSendNotification);
        bufferSizeDropDown->onChange = [this] { updateConfig (false, false, false, true); };

I can see the issues, we are currently working on a fix for this problem.

3 Likes

I discovered changes in the behaviour of the AVAudioSession setPreferredIOBufferDuration:error: method. Previously calling this method would synchronously result in an observable change of the AVAudioSession.IOBufferDuration property, and this is what we used to enumerate the available buffer sizes, for which Apple is not providing a direct API.

On iOS 18 however the setPreferredIOBufferDuration:error: appears to behave in an asynchronous way, and its effect is not immediately visible on the IOBufferDuration property. This has very difficult to mitigate consequences on our buffer size enumeration code, because there is no callback on the iOS API which would tell us when the requested buffer change has taken effect. At least I can’t find one.

As a workaround, it is possible to sleep “enough” so that the changes take effect. I’m sharing a patch doing just that. It would be very helpful if you could take a look @aleFX to see if this behaves acceptably with the external devices in your use.

ios18-v2.patch (1.7 KB)

We’ll file a bug report with Apple in the hope that they can point us to a workaround or maybe address this change, but there’s no telling how that will unfold.

It’s interesting that we don’t see this asynchronous behavior on any of our iOS 18 devices, nor have any customers reported any issues.

That’s an interesting data point.

As far as I can tell this isn’t causing any problems with the audio processing itself. When the audio callback function is called the actual buffer size is reported and used.

So in order to notice this you have to spend enough time on the buffer size enumerating / selecting functions. Even I missed the issue on my first testing, but if you just keep enumerating and selecting different buffer sizes you’ll eventually see strange results - all the while audio processing is still carrying on correctly.

Just another data point, we are seeing the same buffer issue which results in choppy audio in our iOS app. I have tried the latest patch and it does not solve the issue. Xcode 16 and iOS 18.0.

I’m checking, I’ll write here a feedback tomorrow

Can you share more about how that’s happening? Is the selected buffer size ending up as 64 despite wanting a larger buffer? Is that causing the choppy audio?

I’ve tested the patch, unfortunately the issue is still there. Sometimes the sample rate also is not set correctly and you can clearly hear pitched up/down sound, I’ve tried with 200 ms instead of 100 but nothing changes.