Music Systems sounds good in FMOD but having delays/ overlaps when used in-game in Unity

Hey there :blush:
As you can see my music systems contains out of 2 blocks. Left contains the complete song, right a minimal version. Everytime certain conditions turn true in Unity, the FMOD Labeled Parameter is switching btw the states “Driving” and “Standing”. In FMOD the transitions sounds smooth. In-Game, specially noticeable on the drums, there will be an overlapping sound. When the transition is done, everything sounds normal again. Although sometimes a track seems to stay delayed until another transition starts.

For me it sounds like a coding problem, but i don’t see what could be wrong there.

Also i had the issue of tracks staying delayed, when i was switching to the pause menu. Here some tracks should be muted. I fixed this by having a parameter for the pause menu with a volume automation which never goes to -endless dB.

Any ideas on this?

Kindest regards!

Synchronisation issues are often caused by streams needing to buffer. Try setting your music assets to not stream in the assets browser.

Thanks for your reply Joseph!

To me it sounds like the problem occurs less often, but still drums can be heard overlaping some times.

This is how my script looks, which is managing the music. The music changes, depending on the speed of the car and if it is standing or not.

———————————————————————————–

using TMPro;
using UnityEngine;
using FMODUnity;
using Studio = FMOD.Studio;

public class MusicManager : MonoBehaviour
{
public static MusicManager Instance { get; private set; }

[SerializeField, Tooltip("Liste der FMOD-Music-Events")]
private EventReference[] musicTracks;

[SerializeField, Tooltip("Textfeld zur Anzeige des aktuellen Songnamens")]
private TMP_Text songNameText;

private Studio.EventInstance currentTrackInstance;
private int currentTrackIndex = 0;
private bool changeSongInputHold = false;

private const string LOCAL_LAYER_PARAM = "VerticalLayerOnRepeat";
private const string LOCAL_MENU_LAYER_PARAM = "MenuMusicLayer";
private const string LOCAL_MUSIC_RESET_PARAM = "Music Reset";

[Header("Music Reset")]
[SerializeField] private float standStillSpeedThreshold = 10f;
[SerializeField] private float standStillRequiredSeconds = 5f;
[SerializeField] private float lowSpeedThreshold = 100f;
[SerializeField] private float lowSpeedRequiredSeconds = 10f;

[Header("Music Reset Rückwechsel / Stabilisierung")]
[SerializeField] private float resumeDrivingSpeedThreshold = 120f;
[SerializeField] private float minStateChangeInterval = 1.0f;
[SerializeField] private float speedCheckInterval = 0.1f;

private float standStillTimer = 0f;
private float lowSpeedTimer = 0f;
private float speedCheckTimer = 0f;
private float lastMusicStateChangeTime = -999f;

private bool musicResetIsStanding = false;

private void Awake()
{
    if (Instance != null && Instance != this)
    {
        Destroy(gameObject);
        return;
    }

    Instance = this;
}

private void Start()
{
    if (musicTracks.Length > 0)
    {
        PlayCurrentTrack();
    }

    ApplyMusicResetState(false, true);

    if (GameStatusController.Instance != null)
    {
        GameStatusController.Instance.OnNewGameModeEnter += HandleGameStateChange;
    }
}

private void Update()
{
    HandleTrackSwitchInput();
    HandleMusicResetLogic();
}

private void HandleTrackSwitchInput()
{
    if (InputManager.Instance == null) return;

    float nextTrackInput = InputManager.Instance.GetFloatValue("InGameNextTrack");
    float previousTrackInput = InputManager.Instance.GetFloatValue("InGamePreviousTrack");

    if (nextTrackInput != 0 && !changeSongInputHold)
    {
        changeSongInputHold = true;
        NextTrack();
    }
    else if (previousTrackInput != 0 && !changeSongInputHold)
    {
        changeSongInputHold = true;
        PreviousTrack();
    }
    else if (nextTrackInput == 0 && previousTrackInput == 0)
    {
        changeSongInputHold = false;
    }
}

private void HandleMusicResetLogic()
{
    if (VehicleController.Instance == null || !currentTrackInstance.isValid())
        return;

    // Nicht jeden Frame prüfen
    speedCheckTimer += Time.deltaTime;
    if (speedCheckTimer < speedCheckInterval)
        return;

    speedCheckTimer = 0f;

    float speed = VehicleController.Instance.carSpeed;

    if (!musicResetIsStanding)
    {

        standStillTimer = (speed <= standStillSpeedThreshold) ? standStillTimer + speedCheckInterval : 0f;
        lowSpeedTimer = (speed < lowSpeedThreshold) ? lowSpeedTimer + speedCheckInterval : 0f;

        bool standstillReady = standStillTimer >= standStillRequiredSeconds;
        bool lowSpeedReady = lowSpeedTimer >= lowSpeedRequiredSeconds;

        if ((standstillReady || lowSpeedReady) && CanChangeMusicState())
        {
            ApplyMusicResetState(true);
        }
    }
    else
    {
        if (speed >= resumeDrivingSpeedThreshold && CanChangeMusicState())
        {
            standStillTimer = 0f;
            lowSpeedTimer = 0f;
            ApplyMusicResetState(false);
        }
    }
}

private bool CanChangeMusicState()
{
    return Time.time >= lastMusicStateChangeTime + minStateChangeInterval;
}

private void ApplyMusicResetState(bool standing, bool force = false)
{
    if (!currentTrackInstance.isValid())
        return;

    if (!force && musicResetIsStanding == standing)
        return;

    float targetValue = standing ? 0f : 1f;
    currentTrackInstance.setParameterByName(LOCAL_MUSIC_RESET_PARAM, targetValue);

    musicResetIsStanding = standing;
    lastMusicStateChangeTime = Time.time;

    Debug.Log($"[MusicManager] {LOCAL_MUSIC_RESET_PARAM} -> {targetValue}");
}

private void PlayCurrentTrack()
{
    StopCurrentTrack();

    currentTrackInstance = RuntimeManager.CreateInstance(musicTracks[currentTrackIndex]);

    currentTrackInstance.setParameterByName(LOCAL_LAYER_PARAM, 100f);
    currentTrackInstance.setParameterByName(LOCAL_MENU_LAYER_PARAM, 1f);

    float musicResetValue = musicResetIsStanding ? 0f : 1f;
    currentTrackInstance.setParameterByName(LOCAL_MUSIC_RESET_PARAM, musicResetValue);

    currentTrackInstance.start();
}

private void StopCurrentTrack()
{
    if (currentTrackInstance.isValid())
    {
        currentTrackInstance.stop(Studio.STOP_MODE.IMMEDIATE);
        currentTrackInstance.release();
        currentTrackInstance.clearHandle();
    }
}

private void NextTrack()
{
    if (musicTracks == null || musicTracks.Length == 0) return;

    currentTrackIndex = (currentTrackIndex + 1) % musicTracks.Length;
    PlayCurrentTrack();
}

private void PreviousTrack()
{
    if (musicTracks == null || musicTracks.Length == 0) return;

    currentTrackIndex = (currentTrackIndex - 1 + musicTracks.Length) % musicTracks.Length;
    PlayCurrentTrack();
}

private void UpdateSongNameUI(string songPath)
{
    if (songNameText != null)
    {
        string songName = System.IO.Path.GetFileNameWithoutExtension(songPath);
        songNameText.text = $"Jetzt spielt: {songName}";
    }
}

private void HandleGameStateChange(GameStatusController.GameState newState)
{
    if (!currentTrackInstance.isValid()) return;

    var uiState = IngameUIManager.Instance?.currentMainUI;

    if (newState == GameStatusController.GameState.InGameMenu ||
        uiState == IngameUIManager.MainUIState.PhotoModeUI ||
        uiState == IngameUIManager.MainUIState.GameOverUI)
    {
        currentTrackInstance.setParameterByName(LOCAL_MENU_LAYER_PARAM, 0f);
    }
    else
    {
        currentTrackInstance.setParameterByName(LOCAL_MENU_LAYER_PARAM, 1f);
    }
}

private void OnDestroy()
{
    if (GameStatusController.Instance != null)
    {
        GameStatusController.Instance.OnNewGameModeEnter -= HandleGameStateChange;
    }

    StopCurrentTrack();
}

}

Hmm… I’m not spotting any glaringly obvious problems in that script… I’ll look at it further.

Have you tried recording a profiler session while connected to your game using live update? If you do, does the issue occur in FMOD Studio when you play back the session in simulate mode?

This also occurs when i playback the FMOD Profiler Session. The music only has 1 instance, but it seems to happen when the amount of voices changes.

Maybe it helps to know, that the singular tracks of a music event in fmod got delayed whenever i went into the pause menu. When i skipped back to resume the game, the track which got muted in the pause menu, was delayed. I fixed this by setting the volume automation in my labeled parameter not to -infinite dB but -75dB.

What happens inside your FMOD event when the game is paused? Does a parameter value change? Do any transitions occur, and do they involve transition timelines?

When the game is paused the local “Continuous Parameter” named “MenuMusicLayer” (you can see on my first screenshot), turns from 1.00 = 0dB; to 0.00 = -75dB. This volume automation is on a Gain Effect of my guitar track for example. This is not a labeled parameter, it is continuous, as i mistakenly wrote in my message before.

I have transition regions, destinations, markers and loops in my timenline but they don’t get triggered when the pause menu is activated. There just the script communicates to turn the continuous parameter to 0 and back up.

I suspect I now know what’s happening.

The most likely explanation, if the instrument is asynchronous, is that setting the channel/voice of the track to -80 dB is causing the channel/voice associated with the instrument to be virtualized. Then, when the channel/voice is later un-virtualized, it starts playing its asset from the beginning, because asychronous instruments assume that you always want to start playing the asset from the same point (by default, the beginning). If this is the case, possible solutions include using a synchronous instrument instead, using quantization and/or start offset to ensure the asset always begins playing from an acceptable point, or preventing the channel/voice from being virtualized (as you have already partly done by setting the track volume to -75 dB instead of -80 dB.

This is a very useful information for me for the future - knowing it will be virtualized in this context and then starts playing from the beginning.

Neither in the pause menu situation or in-game, i have my instruments asychn. This button is greyed out.

For the pause menu issue i didn’t use quantization, but synchronous instruments.

For the main issue, overlapping music files while playing, they already are synchronous instruments and i use quantiztaion as well as 1 bar of crossfade transitions. This 1 bar is exactly where it happens.

SOLUTION:

This issue happened in FMOD STUDIO and FMOD for UNITY Version 2.02!

After updating both to Version 2.03., i can’t replicate the overlapping anymore. I am happy and a bit ashamed that this is the solution. I were avoiding the update in fear of corrupting my project, since it is kinda huge.

If the issue returns, i will reach out again. Thank you Joseph for taking your time and capacities to help me on this concern <3