Unity makes it easy to play sound in a game. The complexity arises when multiple sounds must be played together or when one sound should take priority over another. In this tech tip, we explore P3SAAudio, the sound manager in P3SampleApp.
The source code for P3SAAudio is available at Assets\Scripts\GUI\Audio\P3SAAudio.cs
The numerous comments give a good description of the fields and methods.
P3SAAudio implements the Multimorphic.P3App.GUI.IAudio interface. This lets the P3 SDK control sounds by calling P3SAAudio through known IAudio methods. This forwarding is initialized in P3SAAudio.Start() with the statement:
Audio.audioInterface = this;
The Unity 5.6.7 manual contains a section on audio. Here is a terse summary:
Unity simulates sounds coming from multiple locations in 3D space. An AudioSource is a component that plays a sound at the location of its parent GameObject. An AudioListener is a component that acts like a microphone at the location of its parent GameObject, typically the Main Camera. Technically, the AudioSource sends the sound signal to an AudioMixerGroup. The AudioMixerGroup applies some effects and sends the output signal to another AudioMixerGroup or the AudioMixer. An AudioMixer mixes audio from one or more AudioMixerGroups. The AudioMixer can send its signal to another mixer or to a listener. The output of the listener is what the user hears through the speakers.
Selecting the menu Edit->Project Settings->Audio in Unity brings up the Audio Manager to edit some project-wide audio settings. See the Unity manual for the description of those settings.
The MainMixer is an asset which is present in all scenes. In the Project window, expand Assets > Resources > Sound and click on Mixer. In the Assets window, double-click on MainMixer. This opens the Audio Mixer window.
In the Groups panel on the left, you can see that the Master AudioMixerGroup takes input from 3 AudioMixerGroups (Playlist, Effects, Voice) and the Voice AudioMixerGroup takes input from 4 AudioMixerGroups (Necessary, ShotFeedback, Instruction and Chatter).
P3SAAudio documents the audio mixer groups like this:
Necessary overrides ShotFeedback, Instruction and Chatter.
ShotFeedback overrides Instruction and Chatter.
Instruction overrides Chatter.
When P3SASetup starts, it creates P3SAAudio by instantiating Resources/Prefabs/P3SAAudio.prefab
The P3SAAudio prefab contains a P3SAAudio script, an AudioListener and multiple Playlists. The AudioMixerGroups are passed as arguments to the P3SAAudio script in the Inspector.
Every time a scene finishes loading, P3SAAudio moves its location to the location of the Main Camera in the scene. It also disables the AudioListener on the Main Camera because P3SAAudio already has an AudioListener component and there can only be one active AudioListener in the scene.
P3SAAudio handles the following events:
Evt_SetSoundtrackVolume
Evt_SetBassGain
Evt_ChangePlaylist
Evt_RequestPlaylist
Evt_RemovePlaylistRequests
// Evt_PlaySound
Evt_AbortSound
Evt_AbortSoundsOnMixerGroup
Evt_InterruptiveAbortSoundsOnMixerGroup
Evt_SetSoundtrackVolume and Evt_SetBassGain are sent by P3SASettingsMode.
Evt_ChangePlaylist is never sent in P3SampleApp.
Evt_RequestPlaylist, Evt_RemovePlaylistRequests, Evt_PlaySound, Evt_AbortSound, Evt_AbortSoundsOnMixerGroup, Evt_InterruptiveAbortSoundsOnMixerGroup are sent by P3SAGameMode audio methods:
protected void RequestPlaylist(string playlistName, bool allowCurrentClipToEndBeforeChange=false) protected void RemovePlaylistRequests() protected void RemovePlaylistRequests(string playlistName) protected void PlaySound(string soundName) // this one is broken protected void AbortSound(string soundName) protected void AbortSoundsOnMixerGroup(string mixerGroupName, bool interruptiveAbort=false) |
P3SAGameMode.PlaySound() is nonfunctional since the Evt_PlaySound event handler is commented out in P3SAAudio. This is acceptable because none of these P3SAGameMode methods are used in P3SampleApp. Instead, the code makes direct calls to P3SAAudio.Instance.<methodName>(…)
P3SAAudio responds to changes in the master volume by setting the volume on the Master AudioMixerGroup. The VolumeControl component in the SDK sets the master volume through the IAudio interface method SetMasterVolumeLevel().
P3SAAudio responds to changes in the soundtrack volume by setting the volume on the Playlist AudioMixerGroup. P3SASettingsMode sets the soundtrack volume by sending the Evt_SetSoundtrackVolume event.
P3SAAudio responds to changes in the bass gain by setting the MasterBassGain parameter on the Master AudioMixerGroup. P3SASettingsMode in P3SampleApp sets the bass gain by sending the Evt_SetBassGain event.
A playlist is used for background music in a scene or a mode. A Playlist is a component that defines a named list of AudioClips that will all play one after the other. You must define the Playlist as a component at the root of the P3SAAudio prefab. No other parent will work.
You can show P3SAAudio in the Inspector by clicking on Assets/Resources/Prefabs in the Project window and then clicking on P3SAAudio in the Asset window.
P3SAAudio comes with 5 playlists named LaunchReady, StandardPlay, MultiballA, MultiballB, and Silence.
In the Inspector, you specify the size of the playlist, and that number of Elements will appear. To assign an AudioClip to an Element:
P3SAAudio’s playlists all have a single AudioClip, except Silence, which has no AudioClips.
This is how the PlaylistRequest feature works. The requesting mode sends the Evt_RequestPlaylist event with a PlaylistRequest event data to add a playlist request. The PlaylistRequests are sorted, and the playlist belonging to the highest-priority mode is played. Ensure that Evt_RemovePlaylistRequests is sent when the mode stops. If the currently playing playlist request belongs to the requesting mode, it will stop the playlist and switch to the playlist from the next mode with the highest priority.
When a Playlist is aborted by another Playlist, it remembers the current AudioClip and the time position within the clip. If the Playlist is resumed later, it will start where it left off.
In P3SampleApp, MultiballMode, HomeMode and TiltedMode make playlist requests. TiltedMode requests the empty Silence playlist, which turns off the music.
P3SAAudio plays the playlist on the AudioMixerGroup named Playlists. P3SAAudio creates two identical AudioSources and stores them in the playlistSources variable. Every time a new Playlist starts playing, P3SAAudio switches to the other AudioSource in playlistSources. Maybe this is to give time for the first AudioSource to fade out the signal when it was stopped.
There is no coroutine to monitor the Playlist. P3SAAudio checks the progress of the currently playing playlist in the Update() method. If the AudioSource is not busy, it triggers the next clip in the playlist.
VariableAudioClip is a component that specifies a clip name with an optional range of pitch and volume to give a little variance to the clip every time it is played.
AudioClipGroup is a component that collects VariableAudioClip components from siblings and children to create an ordered list of clips. The weight of the VariableAudioClip is the number of times it appears in the list. The list can be kept sequential or shuffled in random order. An AudioClipGroup represents a set of alternative audio clips. Only one clip will be played at a time. When the audio group is played, this plays the current VariableAudioClip and it moves the index to the next clip to prepare for the next time it is called. When the list is exhausted, the list is recreated and the index resets to the front of the list.
For example, if you have 3 variants for a sound effect, you can create an AudioClipGroup anywhere in the scene hierarchy and give it 3 VariableAudioClip children. The app can play the audio group to play one of the clips. The next time the app plays the audio group, this can play the same clip or a different one. How frequently a clip plays depends on its weight relative to the weights of the other clips.
AudioClipGroup takes as parameter which AudioMixerGroup to use to play the sound. This is what determines whether the clip is processed as a sound effect or a voice prompt.
FollowUpAudioClipGroup is a component that can be added as a sibling of a VariableAudioClip to specify another AudioClipGroup that should be played after the first VariableAudioClip has finished playing (with a possible delay in between). If a FollowUpAudioClipGroup is a sibling of an AudioClipGroup, it becomes the default FollowUpAudioClipGroup for any child VariableAudioClips that do not have their own FollowUpAudioClipGroup.
AudioClipLimiter is a component that can be added to a scene to limit how many clips can play simultaneously underneath it in the scene hierarchy. The feature is never used in P3SampleApp.
In P3SampleApp, the Home scene has these AudioClipGroups under the path Audio:
MovingTarget is the mode that implements the rotating cube target in P3SampleApp. When the mode detects a collision with the ball, it bumps the cube, changes its color and plays the GroupTest audio group.
P3SAAudio.Instance.PlaySound("MovingTargetHit"); |
You will hear SoftMalletCymbalRoll from MovingTarget3 more frequently because it has a weight of 5 compared to 1 for MovingTarget1 and MovingTarget2. When FX_AA_Hits_sm01 plays from MovingTarget1, it is followed by FX_BallEjectToPlayer_ three seconds later because of the FollowUpAudioClipGroup pointing to MovingTargetFollowUp AudioClipGroup.
It is also possible to play an AudioClip by name without creating an AudioClipGroup. In this case, the sound will have no pitch or volume variations. For example, this loads and caches the sound resource StartGame_PlayerOne.wav
P3SAAudio.Instance.PlaySound3D("StartGame_PlayerOne", gameObject.transform); |
To play a sound, P3SAAudio creates a SoundChain. This class is an implementation detail that is not exposed by the API. The chain holds the initial AudioClip or AudioClipGroup and all the transitive FollowUps, plus a dedicated AudioSource to emit the sound. A new coroutine is started to play the chain, yielding for the duration of each clip. Before moving on to the next clip, the coroutine checks whether it was aborted and needs to stop. If not, it plays the next clip. If the chain’s mixer group overrides other mixer groups, those mixer groups are stopped before playing the next clip.