iOS AudioSession vs mixer

Hello! We encountered a problem with iOS AudioSession using FMOD.

You provide a guide on handling sessions, but I checked your iOS examples, and since version 2.02.23, they no longer follow that guide. The difference is that in newer versions, there are no mixerSuspend/mixerResume calls when the application moves to the background/foreground or when an audio session begins/ends an interruption.

I tested the simple_event example in different versions, and here are the results:

  • Version 2.02.23 and later (including 2.03.05):
    You can start sound by pressing B, then switch to the background (sound stops) and return to the foreground (sound continues playing) — which works fine. However, if you receive an incoming call or alarm and then dismiss it, the sound freezes. The application continues updating as usual (including the mixer), but no sound plays. Additionally, you cannot start any other sounds after that.
    I uploaded the video 20305.mp4 to my profile for reference. iOS version was 17.6.1
  • Version 2.02.22:
    In this version, mixerSuspend/mixerResume calls are present. However, sometimes an “interruption began” event is triggered when the application returns to the foreground, causing a mixerSuspend call and making the sound freeze. Unlike newer versions, if you switch to the background and then return to the foreground, the sound resumes playing.
    You can check video 20222.mp4 and the log file 20222log.rtf for details. iOS version was 13.5.1, it much more difficult to catch it on newer version but it’s possible.

So my question is: What is the proper way to handle iOS audio sessions, application suspensions, and activations?

Hi,

There were some changes to how FMOD handles interruptions and suspend/resume 2.02.23/ 2.03.02, which would explain the differences you’re observing between the versions you noted. It’s likely that the platform-specific docs weren’t updated to follow. I’ll take a look at your uploaded videos, our docs, and the expected behavior, and get back to you on this soon.

Thanks for the videos and log clearly demonstrating what you’re talking about.

To succinctly answer your question: the iOS platform specific docs are correct.

To expand on this: as part of the changes I noted with 2.02.23, our docs now state that “it is now the responsibility of the developer to interact with the AudioSession APIs native to this platform”. Essentially, we expect users to implement mixerSuspend/mixerResume calls themselves. This could be done in a few ways:

  • A pointer to the FMOD system object coreSystem can be passed to the iOS platform .mm code to call mixerSuspend/mixerResume, like it is in the docs
  • A pointer to a C++ function that calls mixerSuspend/mixerResume can be passed to the to the iOS platform .mm, similar to how FMOD for Unity handles this

However, the FMOD Engine examples are required to remain platform-agnostic, so the code example listed in the docs isn’t present - this is the discrepancy you’ve noted.

Personally, I do find it strange that the FMOD Engine example code doesn’t handle the suspend/resume calls, or doesn’t communicate the expectation that the user do so in some way. I’ve passed this along to the development team for further consideration.

Apple documentation offers specific techniques for handling Audio Session interruptions, and none of them suggest manually stopping sounds. The Audio Session handles this automatically for you. However, as you can see from your latest examples, FMOD does not handle such interruptions correctly—it stops playing entirely instead.

On the other hand, we can call mixerSuspend/mixerResume when necessary to help the FMOD engine handle such interruptions, but you need to provide an example of how to do it properly.

As previously stated, this is intentional in order to have the code remain platform-agnostic. I do think this should be changed though, and I have flagged some improvements with the development team.

The example in our documentation demonstrates how to do this. To get it to work with the FMOD Engine examples, you need to do the following:

  • Place the mixerSuspend/mixerResume calls in the documentation example in the same addObserverForName calls in common_platform.mm
  • Pass a pointer to your core FMOD system to Common_Init() as the extraDriverData argument
  • In Common_Init(), cast extraDriverData back to FMOD_SYSTEM* and store it as coreSystem so it can be called from within common_platform.mm

The example in our documentation demonstrates how to do this.

Such way is implemented in FMOD examples version 2.02.22 and earlier. Initially I’ve also uploaded the video with the work of this example, you can check it (20222.mp4 and 20222log.rtf). It also doesn’t work properly. The problem is here:
There is no guarantee that a begin interruption will have a corresponding end interruption
But your docs and examples expects session calls in pairs.

So there is no problem of passing fmod system object to the right place or call mixerSuspend/mixerResume in the way as documentation says, there problem is that such way is also doesn’t work.

Apologies, I misunderstood what you were saying.

I’ve flagged this with the development team, and I’ll get back to you with a more definitive example of how to handle this soon.

Hello, have you discussed the problem with the development team? Do you have any plans to solve it? It’s a critical issue for us, so we are looking forward to a solution.

An additional Core API example demonstrating how to integrate mixer suspension with our example code, and documentation improvements, are currently being worked on. I cannot provide a timeline for when they’ll be released, but I’d be happy to share the example code as it is now with you. It shouldn’t require any modification to other FMOD code in the examples like common and common_platform.

Essentially, the sample code creates a callback function to pass to the Obj-C code in common_platform.mm with Common_RegisterSuspendCallback(). This callback sets a global bool indicating whether the mixer is to be suspended/resumed. The global bool is checked against in the main loop to ensure that the mixer suspend/resume call is done from the main thread:

#include "fmod.hpp"
#include "common.h"
void FMOD_SuspendResumeCallback(bool suspend);
void FMOD_HandleSuspendResume(FMOD::System *system);
bool gSuspend = false;
int FMOD_Main()
{
    FMOD::System     *system;
    FMOD::Sound      *loopingSound;
    FMOD::Channel    *channel = 0;
    bool              paused = true;
    FMOD_RESULT       result;
    bool              suspended;
    void             *extradriverdata = 0;
    
    Common_Init(&extradriverdata);
    /*
        Register mixer suspend/resume callback with platform code to handle interruption or suspension by OS
    */
    Common_RegisterSuspendCallback(FMOD_SuspendResumeCallback);
    
    /*
        Create a System object and initialize
    */
    result = FMOD::System_Create(&system);
    ERRCHECK(result);
    result = system->init(32, FMOD_INIT_NORMAL, extradriverdata);
    ERRCHECK(result);
    result = system->createSound(Common_MediaPath("drumloop.wav"), FMOD_DEFAULT, 0, &loopingSound);
    ERRCHECK(result);
    
    result = system->playSound(loopingSound, 0, true, &channel);
    ERRCHECK(result);
    /*
        Main loop
    */
    do
    {
        Common_Update();
        
        /*
            Handle suspending/resuming mixer
        */
        {
            if (gSuspend && !suspended)
            {
                result = system->mixerSuspend();
                ERRCHECK(result);
                if (result == FMOD_OK)
                {
                    suspended = true;
                }
            }
            else if (!gSuspend && suspended)
            {
                result = system->mixerResume();
                ERRCHECK(result);
                if (result == FMOD_OK)
                {
                    suspended = false;
                }
            }
        }
        
        
        if (Common_BtnPress(BTN_ACTION1))
        {
            if (paused){
                result = channel->setPaused(false);
                paused = false;
            }
            else
            {
                result = channel->setPaused(true);
                paused = true;
            }
            ERRCHECK(result);
        }
        result = system->update();
        ERRCHECK(result);
        
        {
            Common_Draw("==================================================");
            Common_Draw("Suspend Resume Example.");
            Common_Draw("Copyright (c) Firelight Technologies 2004-2025.");
            Common_Draw("==================================================");
            Common_Draw("");
            Common_Draw("Press %s to pause/unpause looping sound (drumloop)", Common_BtnStr(BTN_ACTION1));
            Common_Draw("Sound status: %s", paused ? "Paused" : "Playing");
            Common_Draw("Mixer status: %s", suspended ? "Suspended" : "Active");
            Common_Draw("Press %s to quit", Common_BtnStr(BTN_QUIT));
        }
        Common_Sleep(50);
    } while (!Common_BtnPress(BTN_QUIT));
    /*
        Shut down
    */
    result = loopingSound->release();
    ERRCHECK(result);
    result = system->close();
    ERRCHECK(result);
    result = system->release();
    ERRCHECK(result);
    Common_Close();
    return 0;
}
void FMOD_SuspendResumeCallback(bool suspend)
{
    /*
        Set global bool to defer suspend/resume call to main thread
    */
    gSuspend = suspend;
}

Using this code, I haven’t been able to reproduce any of the issues you previously described, or demonstrated in videos. Give it a try and let me know whether it works as expected.

I modified api/core/examples/play_sound.cpp with the code you provided in the last message. There is still the problem. We can reproduce it only on iOS 13.5.1. I’ve uploaded logs(play_soung_logs.txt) and video(play_soung_video.txt, change txt to mov to watch).

But I think it’s not the main problem we have. Please pay attention to my initial message in this thread. The second Version 2.02.22: it can be situation when “interruption began” call goes after “did become active” call. And it pauses playback until the next background. You can check it in the logs I’ve initially uploaded. We can also reproduce it only on iOS 13.5.1 but our users experience it on other iOS versions.
As apple docs says says during the audio session interruption you shouldn’t pause playback by yourself, the system takes care of it.

Thank you for the uploads - I will do some more tests on what you’ve described and get back to you soon.

To confirm: does this only happen on 2.02.22? As I mentioned before, a number of changes were made in 2.02.23 that should have fixed this behavior.

Yes, the system automatically deactivates the AudioSession. However, the FMOD mixer thread will continue to run in the background even when the AudioSession is deactivated, which is why the mixer has to be suspended/resumed.

It doesn’t depend on particular FMOD version. I mentioned 2.02.22 because it’s the last version where examples contain mixer suspend/resume calls. But as I described your last example also has the same issue on 2.02.27 version. And the reason why it happens I’ve also described:

it can be a situation when “interruption began” call goes after “did become active” call. And it pauses playback until the next background.

Sorry, I was unclear on the exact reason you were making the distinction between FMOD versions - thanks for the clarification.

Thanks for your patience.

I’ve done some more testing, but presumably due to iOS differences and given that iOS 13 is no longer supported by Apple, I cannot reproduce the issue. What versions have your users been able to reproduce it on? Have they run into it on versions above iOS 15? If so, which?

With that said, I can see how the particular combination of receiving a “interruption began” after a session has already become active, but without a corresponding “interruption end”, could cause problems. For this case, all I can recommend is to set the AudioSession as active and resume the mixer when the user performs an input. As a simple demonstration, I’ve modified common_platform.mm in our iOS examples as follows:

  • Created a boolean appIsActive to track whether the app is active or not:
bool gIsSuspended = false;
bool gNeedsReset = false;
bool isAppActive = false;     // new code here
  • Set appIsActive to true at the end of Common_Init:
/*
    Activate the audio session
*/
success = [session setActive:TRUE error:nil];
assert(success);
appIsActive = true;    // new code here
  • Set appIsActive to true in the existing UIApplicationDidBecomeActive observer:
[[NSNotificationCenter defaultCenter] addObserverForName:UIApplicationDidBecomeActiveNotification object:nil queue:nil usingBlock:^(NSNotification *notification)
{
    NSLog(@"Application did become active");
    appIsActive = true;    // new code here
// ...
}
  • Set appIsActive to false in the existing UIApplicationWillResignActive observer:
[[NSNotificationDenter defaultCenter] addObserverForName:UIAppliCationWillResignActiveNotification object:nil queue:nil usingBlock:^(NSNotification *notification)
{
    NSLog(@"Application will resign active");
    appIsActive = false;    // new code here
}
  • At the end of update, if the app is active and the user performs inputs, try to activate the AudioSession and resume the mixer:
void (update)
{
    
    // ...
    gButtonsPress = ((gButtonsDown ^ gButtons) & gButtons) | gActions;
    gButtonsDown = gButtons;
    gActions = 0;

    // new code start
    if (gButtons != 0 && appIsActive)
    {    if ([[AVAudioSession sharedInstance] setActive:TRUE error:nil])
        {
            gIsSuspended = false;
            gSuspendCallback(false);
        }
    }
    // new code end
}

This should resolve the issue, but I can’t be sure since I’m not able to reproduce the issue on my end. However, I’ve also added this particular case, and the advice to check for inputs to activate the AudioSession, to the improvements we’re making to the iOS platform docs and examples that I mentioned previously.

Our users experience such issues on various iOS versions, including those above 15.
Your example of a potential fix fails when you receive an incoming call/alarm the notification panel will appear above the screen. At that moment, the app will receive an ‘interruption began’ event, but you can continue playing. It would be incorrect to reactivate the app’s audio session during the call, you should wait until “interruption end” comes.

As stated in the Apple docs: “There is no guarantee that a begin interruption will have a corresponding end interruption. Your app needs to be aware of a switch to a foreground running state or the user pressing a Play button. In either case, determine whether your app should reactivate its audio session”.

If the following things happen at once, which seems to be what is happening on your end:

  • The UIApplication is already active
  • An interruption begins without the UIApplication becoming inactive or backgrounded
  • No interruption end comes

then there is unfortunately no way for FMOD to recover and reactivate the AudioSession without checking for user input or using a timer.

However, just to be sure, could I get you to add a variable UIApplicationState lastState, and the following new code to update to log the UIApplicationState, to common_platform.mm:

UIApplicationState lastState;

//...

(void) update
{
    //...
    UIApplicationState state = [UIApplication sharedApplication].applicationState;
    if (lastState != state) {
        NSLog(@"applicationState = %ld", state);
        lastState = state;
    }
    //...
}

And upload another log where the issue occurs?

You misunderstood me a bit. The problem with the last example is not that ‘no interruption end comes,’ but that you will be constantly trying to reactivate the audio session during the phone call. This will eventually result in a successful reactivation, causing the game’s sound to play during the call, which is bad.

In my experience, another method actually works: you should try reactivation only once. If the system’s interruption is fake or incorrect, the audio session will be activated on the first attempt; otherwise, it’s a real interruption.

To confirm - do you find that attempting to reactivate the AudioSession only once like you’ve said resolves the issue you were running into?

That said, thank you for the suggestion. I will do some testing to see which method of reactivating the AudioSession is best for our iOS examples, and update our documentation accordingly.