Skip to content
DEV505 / Sound ManagerDOCUMENTATION

DEV505 / CODE RECIPE

Crossfade two allowed music tracks.

Supply Track before playback, fade through two retained handles, and retire the outgoing play only after it becomes silent.

Start with Your first sound if you have not set up a sound scene before. Use a separate test scene with one enabled Sound Manager, generated Sound Keys, and one Audio Listener near the sound object. Add Jukebox from 08-Jukebox to the manager's Direct events list; these are the supplied .soundgraph assets under Assets/DEV505/SoundManager/Samples.

Save the complete code below as TwoTrackMusic.cs in Assets/Scripts. Wait for Unity to compile it, create an empty GameObject, and drag the script onto it. The filename must match the class name. Test one recipe component at a time so another demo controller does not compete for the same events.

Drag Jukebox_MintMorning.wav into First Track and Jukebox_PeachSunset.wav into Second Track. Both are in 08-Jukebox/Music and in the graph’s allowed Track list. Leave Volume at 0.65 to begin.

In Play Mode, wait until the Sound Manager is ready. To run a listed test action, open the component's three-dot menu in the Inspector and choose that action. These are the script's ContextMenu methods. Your own code or a Unity UI Button can call its public methods too. Inspector edits made during Play Mode are temporary.

Choose Play first track and wait about three seconds. Choose Play second track. Listen as the first recedes and the second rises over about 2.5 seconds. During that transition, choose Pause both tracks, wait, then Resume both tracks. Both songs and their fade should continue together.

Wait for the transition to finish before selecting again. This small recipe refuses another selection while its outgoing play is alive. The full demo instead remembers the latest requested selection. Stop music now and disabling the component clean up immediately; the full demo also has a gradual normal-stop interaction.

The Track Asset must belong to the graph’s allowed list. The initial context also supplies Gain 0 and the chosen Volume. If the new request is refused, the existing song is left alone. Only after acceptance does the component hand the old handle to outgoing and make the new one current.

It then sets the old Gain target to 0 and the new target to 1. The graph smooths Gain at 0.4 units per second and shapes it with a sine curve for an equal-power blend. This is a volume transition, not beat matching.

Update reads the outgoing Gain’s Current value. Once it is at silence, it stops that play and clears the outgoing handle. Checking Target would stop it immediately because the target became zero as soon as the fade began.

Volume is updated on both handles so the slider does not change the fade balance. Pause also reaches both. While paused, the recipe skips fade retirement; neither a paused song nor a paused parameter ramp should advance simply because wall-clock time has passed.

This component has no automatic next-track timing, song progress estimate, or dancer. Add those gameplay features around this two-play lifecycle if you need them. Keep the two-instance graph limit in mind.

TwoTrackMusic.cs
using DEV505.SoundManager;
using UnityEngine;
public sealed class TwoTrackMusic : MonoBehaviour
{
[SerializeField] private AudioClip firstTrack = null;
[SerializeField] private AudioClip secondTrack = null;
[SerializeField, Range(0, 1)] private float volume = 0.65f;
private ISoundService service;
private SoundOwnerToken owner;
private SoundHandle current;
private SoundHandle outgoing;
private AudioClip currentClip;
private bool paused;
[ContextMenu("Play first track")]
public void PlayFirst() => Select(firstTrack);
[ContextMenu("Play second track")]
public void PlaySecond() => Select(secondTrack);
public void Select(AudioClip clip)
{
if (!Application.isPlaying || !isActiveAndEnabled || !clip || paused) return;
var active = SoundServices.Current;
if (active == null || !active.Ready) return;
if (!ReferenceEquals(service, active))
{
current = outgoing = default;
service = active;
owner = service.OwnerOf(this);
}
if (current.IsAlive() && currentClip == clip) return;
if (outgoing.IsAlive())
{
Debug.Log("Wait for the current crossfade to finish before choosing again.", this);
return;
}
var context = SoundPlayContext.At(transform.position, owner)
.With(SoundParameters.Jukebox.Track, clip)
.With(SoundParameters.Jukebox.Gain, 0f)
.With(SoundParameters.Jukebox.Volume, volume);
var result = SoundBus.Play(Sounds.Jukebox, context);
if (!result.Accepted)
{
Debug.LogWarning($"Track could not start: {result.Status}", this);
return; // Keep the current track if the new request failed.
}
outgoing = current;
current = result.Handle;
currentClip = clip;
if (outgoing.IsAlive()) SoundBus.SetFloat(outgoing, SoundParameters.Jukebox.Gain, 0);
SoundBus.SetFloat(current, SoundParameters.Jukebox.Gain, 1);
}
private void Update()
{
if (current.IsAlive()) SoundBus.SetFloat(current, SoundParameters.Jukebox.Volume, volume);
if (outgoing.IsAlive()) SoundBus.SetFloat(outgoing, SoundParameters.Jukebox.Volume, volume);
if (paused || !ReferenceEquals(service, SoundServices.Current)) return;
if (outgoing.IsAlive() && service.TryGetParameter(outgoing, SoundParameters.Jukebox.Gain, out var gain)
&& gain.Current.number <= 0.001f)
{
SoundBus.Stop(outgoing, 0); // The graph has already faded this play to silence.
outgoing = default;
}
}
public void SetPaused(bool value)
{
paused = value;
SoundBus.SetPaused(current, value);
SoundBus.SetPaused(outgoing, value);
}
[ContextMenu("Pause both tracks")]
public void PauseMusic() => SetPaused(true);
[ContextMenu("Resume both tracks")]
public void ResumeMusic() => SetPaused(false);
[ContextMenu("Stop music now")]
public void StopMusic()
{
if (ReferenceEquals(service, SoundServices.Current) && owner.IsValid)
service.StopOwned(owner, 0);
current = outgoing = default;
currentClip = null;
paused = false;
}
private void OnDisable()
{
StopMusic();
if (ReferenceEquals(service, SoundServices.Current) && owner.IsValid)
service.ReleaseOwner(owner);
owner = default;
service = null;
}
}

Read the demo walkthrough for the graph, parameters, controls, and Unity screenshots. The related API reference explains the calls used here. Return to all twelve recipes to choose another interaction.

This page imports the checked C# file directly. It was compiled against the documented Unity revision; its runtime checks and source hash are recorded in the documentation repository’s validation files.