Non-looping virtual channels stay virtual forever and don't ever stop or get paused

I’m having problems managing the lifetime of non-looping, virtual fmod channels right now. This is what I currently do in my engine:

  • create a channel with FMOD_System_PlaySound
  • create custom bookkeeping for that sound
  • poll the channel with FMOD_Channel_IsPlaying every frame. If the return value is not FMOD_OK or isPlaying is false, I consider the channel to have finished playback (because to my understanding these are the symptoms of FMOD cleaning the channel up, which only happens after it has finished playback)
  • if I consider a channel to have finished playback, I free my custom bookkeeping and call FMOD_Channel_Stop on it.

I have a debug-mode that delays the cleanup of custom bookkeeping and the FMOD_Channel_Stop call, so I can look at state in an ingame UI. I can then at some point let the deletions and FMOD_Channel_Stop-calls catch up. If I delay too many deletions, some channels will start to go virtual, and on catch- up, they won’t ever be cleaned up. On further inspection, this is because they are still playing and still non-paused, even after they should have finished (virtual) playback.

The basic goal I want to achieve with this is to know when a channel has finished playback, regardless whether it is virtual or not. Right now, the failure to tell if a virtual sound has finished playback leads to my engine keeping the bookkeeping around after debugging, and it starts piling up. I haven’t encountered this in normal gameplay yet, but believe this might happen there aswell if too many channels play at the same time, and that would be bad news and a resource-leak.

What am I doing wrong? How can I tell if a non-looping virtual channel has finished playback?

Can you tell me which version of FMOD you are using?

I’m not able to reproduce that behavior here but if you can supply some examples of your code I may have more luck.

As far as I can tell from your description everything appears fine, and in my testing it all works as expected. I also used the FMOD Profiler to watch the channels being created/virtualized/destroyed. One thing that did come up is that you won’t be able to call stop on channels if they have been stolen, you will get an error FMOD_ERR_CHANNEL_STOLEN, but calling ->isPlaying on it will return false.

I’m using core api version 2.02. The code excerpts below are in a language called jai (which binds to fmod via its C-API). I hope that doesn’t cause too much confusion, as the syntax doesn’t stray too far from C/C++.

platform_init_audio is called on application startup. The channels are created with platform_create_audio_playback. Every frame, update_audio_input is run, which collects all invalid fmod channels and notifies the game about them. The game then calls platform_destroy_audio_playback on the invalid fmod channels it was notified about, unless I activate the debug mode I mentioned. In that case, all calls to platform_destroy_audio_playback will be delayed until the debug mode is disabled.

VERIFY_OK_FMOD asserts if the return value is not FMOD_OK.

platform_init_audio :: (){
    PROFILE_FUNCTION();
    {
        PROFILE_SCOPE("FMOD_System_Create");
        VERIFY_OK_FMOD(FMOD_System_Create(*fmod_system, FMOD_VERSION));
    }
    {
        PROFILE_SCOPE("FMOD_System_Init");
        VERIFY_OK_FMOD(FMOD_System_Init(fmod_system, 512, FMOD_INIT_NORMAL, null));
    }

    fmod_sounds.allocator = context.persistent_allocator;
    is_audio_initialized = true;
    is_mixer_active = true;
}
platform_create_audio_playback :: (asset_handle: Audio_Asset_Handle, volume: f32) -> Audio_Playback_Handle{
    DASSERT(is_audio_initialized);

    sound := get_existing(*fmod_sounds, asset_handle).*;
    channel: *FMOD_CHANNEL;
    handle := next_audio_playback_handle;
    
    VERIFY_OK_FMOD(FMOD_System_PlaySound(fmod_system, sound, null, 1, *channel));
    VERIFY_OK_FMOD(FMOD_Channel_SetVolume(channel, volume));
    VERIFY_OK_FMOD(FMOD_Channel_SetVolumeRamp(channel, 1));
    VERIFY_OK_FMOD(FMOD_Channel_SetPaused(channel, 0));

    insert(*fmod_channels, handle, channel);
    next_audio_playback_handle += 1;
    return handle;
}
platform_destroy_audio_playback :: (handle: Audio_Playback_Handle){
    DASSERT(is_audio_initialized);
  
    channel := get_existing(*fmod_channels, handle).*;
  
    //we call this whether the channel is valid or not, which seems to work so far.
    VERIFY_OK_FMOD(FMOD_Channel_Stop(channel));
  
    remove_existing(*fmod_channels, handle);
}

update_audio_input :: (){
    for fmod_channels
        if !is_channel_valid(it)
            array_insert(*platform_input_state.finished_audio_playbacks, it_index);
}
is_channel_valid :: (channel: *FMOD_CHANNEL) -> bool{
    is_playing: s32;
    result := FMOD_Channel_IsPlaying(channel, *is_playing);
    return result == .FMOD_OK && is_playing == 1;
}

If you don’t see anything wrong with this code, I could try to create a minimal repro for further debugging/isolation, but that would take some time.

I cannot see anything that jumps out at me immediately, you have probably seen it already but I’ll link our main doc page about virtual voices in case that helps at all: https://www.fmod.com/docs/2.03/api/white-papers-virtual-voices.html.

Non-looping sounds should naturally end by themselves, but are you saying that yours aren’t? Is that for every sound or only some?
Are you using any dsp effects that could be keeping the channels alive?

Channels are stopped automatically when their playback position reaches the length of the Sound being played. This is not the case however if the Channel is playing a DSP or the Sound is looping, in which case the Channel will continue playing until stop is called. Once stopped, the Channel handle will become invalid and can be discarded and any API calls made with it will return FMOD_ERR_INVALID_HANDLE.