Skip to content
DEV505 / Sound ManagerDOCUMENTATION

DEV505 / START HERE

Ask for an action. Hear when it ends.

Use a signal to trigger the horn and a notification to learn what happened to a play.

A signal asks an already running sound to do something. A notification tells your code what happened to a play. They travel in opposite directions.

For example, send Horn to the Engine event to ask it to sound the horn. Listen for a terminal notification to know when the whole engine play ends. “Terminal” means that play has finished, been cancelled, or failed.

This excerpt needs a retained Engine handle named engine:

Ask the Engine graph for its horn · excerpt
bool accepted = SoundBus.SendSignal(engine, "Horn");

The graph must contain On Signal nodes with that exact name. A successful call queues the signal for the next service update. It does not start another Engine play. False means the play ended or is stopping, the signal name is missing, or its signal queue is full.

For frequent signals, use SoundBus.TryResolve to find the registered event, then eventAsset.GetSignalId("Horn") once. Check the ID’s IsValid property before using it. Signal IDs are resolved from events; the current key generator does not create a SoundSignals class.

A graph that should wait for signals while otherwise silent needs Keep alive or other pending work. Otherwise it may finish before you send the signal.

  1. Set up a separate test scene with a manager and listener.
  2. Add Samples/04-Engine/Engine.soundgraph to the manager’s Direct events, then generate keys.
  3. Save this code as EngineSoundControls.cs under Assets/Scripts. Attach it to an empty GameObject near the listener. No object references need to be assigned.
  4. Press Play. The engine starts. Change Rpm in the Inspector to hear its layers change.
  5. Open the component menu and choose Sound horn, Pause engine, Resume engine, or Stop engine. Use Start engine to start again after it ends.
  6. With the Console open, stop the engine and watch for the ending message. A normal explicit stop reports Cancelled; that label does not mean something went wrong.
EngineSoundControls.cs · complete component
using System;
using DEV505.SoundManager;
using UnityEngine;
public sealed class EngineSoundControls : MonoBehaviour
{
[SerializeField, Range(0f, 1f)] private float rpm = 0f;
private SoundHandle engine;
private IDisposable subscription;
private void OnEnable()
{
subscription = SoundBus.Notifications.Subscribe(OnSoundChanged);
}
private void Start() => StartEngine();
[ContextMenu("Start engine")]
public void StartEngine()
{
if (!Application.isPlaying || !isActiveAndEnabled || SoundBus.IsAlive(engine))
return;
var service = SoundServices.Current;
if (service == null)
{
Debug.LogWarning("Add an active Sound Manager to the scene.", this);
return;
}
var context = SoundPlayContext.Following(transform, service.OwnerOf(gameObject))
.With(SoundParameters.Engine.Rpm, rpm)
.With(SoundParameters.Engine.Load, 0.2f);
var result = SoundBus.Play(Sounds.Engine, context);
if (result.Accepted)
engine = result.Handle;
else
Debug.LogWarning($"Could not start Engine: {result.Status}", this);
}
private void Update()
{
if (SoundBus.IsAlive(engine))
SoundBus.SetFloat(engine, SoundParameters.Engine.Rpm, rpm);
}
[ContextMenu("Sound horn")]
public void SoundHorn()
{
if (SoundBus.IsAlive(engine))
SoundBus.SendSignal(engine, "Horn");
}
[ContextMenu("Pause engine")]
public void PauseEngine() => SoundBus.SetPaused(engine, true);
[ContextMenu("Resume engine")]
public void ResumeEngine() => SoundBus.SetPaused(engine, false);
[ContextMenu("Stop engine")]
public void StopEngine()
{
if (SoundBus.IsAlive(engine))
SoundBus.Stop(engine);
// Keep the handle until the ending notification so it can be identified.
}
private void OnSoundChanged(SoundNotification message)
{
if (!message.Handle.Equals(engine) || !message.IsTerminal)
return;
Debug.Log($"Engine ended: {message.Kind} ({message.Reason})", this);
engine = default;
}
private void OnDisable()
{
subscription?.Dispose();
subscription = null;
StopEngine();
engine = default;
}
}

This script controls audio only. It does not move a car. Use it in its own test scene rather than alongside the Engine demo’s existing controller.

Subscribe returns an IDisposable: a token with a Dispose() method that removes your callback. The component subscribes in OnEnable and disposes it in OnDisable. This prevents disabled components from continuing to receive messages.

The bus reports all plays, including those started by other components. message.Handle.Equals(engine) filters for this script’s engine. message.IsTerminal filters for an ending. The script then clears the saved handle so another start is allowed.

StopEngine() deliberately keeps the handle until the ending notification arrives. Clearing it immediately would make that callback’s handle comparison fail. During disable, the script unsubscribes first and then clears its own state.

The built-in service collects notifications and delivers them at the end of a service update on Unity’s main thread. Subscribe before starting sounds, then save the accepted handle. Calls made from inside a notification callback can queue new work for a later update; avoid assuming it will run inside the callback.

The bus follows a replacement active manager within the same play session. Your old handles still belong to the old service and become stale. Subscriptions are cleared when a new Unity play session starts.

An immediate rejected play request usually gives you only its SoundPlayResult, because no play was admitted. Check that result even when you also subscribe to notifications. Later failures of admitted plays are reported as Failed.

Read notification fields and kinds or results and limits for the exact meanings.