Workaround: Trigger transitions for parameter changes over two frames (using Coroutines in Unity)

Continuing the discussion from Transitions latency when changing parameter:


TL;DR
When switching a parameter in a coroutine in two frames, use

FMODUnity.RuntimeManager.StudioSystem.flushCommands();

to force FMOD to change to your parameter on the first frame, start scheduling a transition and THEN evaluate your second parameter change, AFTER the first transition has been started.


THE BUG
I was experiencing an issue with my music system, where the transitions didn’t trigger correctly if I didn’t manually insert some latency by either waiting for about 5 frames or waiting for 30 ms.
The issue was that sometimes the actual audio of the event got stuck on the first playing song. Ie. the switch to my “00 Selection Loop” never happened.


THE SYSTEM
Here’s how my event was set up:

  • I have several songs in nested events, inside the main “musicController”-event.
  • All the songs can be accessed through the “00 Selection Loop” by switching the “Song”-parameter to their appropriate indexes (setup as integers: 01, 02, 03, etc).
  • All songs can go back to the “00 Selection Loop” by switching the “Song”-parameter to 0.

This means that in order to go from Song 02 to Song 04, FMOD would have to do like this:

Song 02 → 00 Selection Loop → Song 04


I implemented this system in Unity by doing the following in a Coroutine:

Frame 0
CurrentParameterValue == 2f
SetParameter to 0f
yield/wait 1 frame (for Fmod to to switch to the Selection Loop)

Frame 1
CurrentParameterValue == 0f
SetParameter to 4f
CurrentParameterValue == 4f


THE BUG - Explanation
As mentioned before, the bug I encountered was that sometimes the actual audio of the event got stuck on the first song. Ie. the switch to the “00 Selection Loop” never happened.

The issue seems to be with how FMOD’s internal asynchronous Update() works with how commands are evaluated and how transition-logic is scheduled:

Big thanks to Derek_Burnheim for identifying the bug and giving the solution.


SOLUTION OF BUG
The final code snippet that did the trick looks like this:
FMODUnity.RuntimeManager.StudioSystem.flushCommands();

And in my system I implemented it like this:

Frame 0
CurrentParameterValue == 2f
SetParameter to 0f
yield/wait 1 frame (for Fmod to to switch to the Selection Loop)

// Force FMOD to evaluate the current parameter and schedule all transitions (ie. actually go to the selection loop):
FMODUnity.RuntimeManager.StudioSystem.flushCommands();

Frame 1
CurrentParameterValue == 0f
SetParameter to 4f
CurrentParameterValue == 4f


Some notes for other devs with similar issues:
The biggest challenge was to find/make a valid FMOD::Studio::System-object which was properly initialised. The solution for that was to use the one provided on the FMODUnity.RuntimeManager.


If anyone’s interested, here’s the code for the entire Coroutine which handles the switch of music/ambience:

	/// <summary>
	/// Simplifies the Fmod-event by allowing the timeline to jump back to zero for a frame, 
	/// then jump to the desired song/ambience.
	/// <para></para>
	/// Also checks if the desired music/ambience is the same as the current one. In that case, it will do nothing.
	/// </summary>
	private IEnumerator SwitchAudioCoroutine(int desiredMusicSong, int desiredAmbienceType)
	{
		// The "_Fmod"-class just contains a bunch of variables with public readonly strings 
		// to avoid someone doing a typo somewhere, and propagate name-changes from one centralized place.
		// So the values are:
		// _Fmod.Params.song == "song"
		// _Fmod.Params.variation == "variation"
		//
		// "musicController" and "ambController" are two FMODUnity.StudioEventEmitter components which
		// contain the relevant music and ambience events, respectively.

        //Just making sure that the value doesn't get truncated (ie. float 0.9 → int 0, which is not the desired result).
		int currentMusicParamValue = Mathf.RoundToInt(AudioManager.Instance.GetParameterValue(musicController, _Fmod.Params.song));
		int currentAmbienceParamValue = Mathf.RoundToInt(AudioManager.Instance.GetParameterValue(ambController, _Fmod.Params.variation));

        //Only go to selection loop if we actually are changing the desired music/ambience
		if (currentMusicParamValue != desiredMusicSong)
		{
			musicController.SetParameter(_Fmod.Params.song, 0);
		}
		if (currentAmbienceParamValue != desiredAmbienceType)
		{
			ambController.SetParameter(_Fmod.Params.variation, 0);
		}

        //Wait a frame for the parameter change to take effect.
		yield return null;

        // HACK: Fmod has latency when switching with parameters and transition regions (https://qa.fmod.com/t/transitions-latency-when-changing-parameter/14383/28)
        // Therefore we flush the fmod thread, so that all the polled commands will be executed first, before continuing Fmod's internal (asynchronous) update.
        // This makes it *actually* register the last frame's parameter change (ie. setting it to 0, which triggers a transition to the "selection loop"), 
        // before changing the parameter to the desired music/ambience.
        FMODUnity.RuntimeManager.StudioSystem.flushCommands();

        //Set the music/ambience to the desired values.
        musicController.SetParameter(_Fmod.Params.song, desiredMusicSong);
		ambController.SetParameter(_Fmod.Params.variation, desiredAmbienceType);
	}

Also, to get the parameter value, here’s the code for that:

public float GetParameterValue(FMODUnity.StudioEventEmitter eventEmitter, string parameterName)
	{
		FMOD.Studio.ParameterInstance parameterInstance;
		float paramValue;

		eventEmitter.EventInstance.getParameter(parameterName, out parameterInstance);
		parameterInstance.getValue(out paramValue);

		return paramValue;
	}

Regards,

Pablo Sorribes Bernhard
contact@pablosorribes.com
pablosorribes.com

2 Likes