Unity makes it easy to play sound in a game. The complexity comes when playing multiple sounds together or one in preference to the other. In this tech tip, we explore P3SAAudio, the sound manager in P3SampleApp.
The source code of P3SAAudio is available at file:///C:/P3/P3_SDK_V0.8\P3SampleApp\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 playing a sound at the location of the parent GameObject. An AudioListener is a component that acts like a microphone at the location of the 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 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 ShotFeeback, 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 three 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 broken since the Evt_PlaySound event handler is commented out in P3SAAudio. That’s ok since none of these P3SAGameMode methods are used in P3SampleApp. Instead the code prefers to make direct calls to P3SAAudio.Instance.<methodName>(…)
P3SAAudio reacts 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 reacts 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 reacts to changes in the bass gain by setting the MasterBassGain parameter on the Master AudioMixerGoup. P3SASettingsMode in P3SampleApp sets the bass gain by sending the Evt_SetBassGain event.
A playlist is used for background music for 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 in 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 click on P3SAAudio in the Asset window.
P3SAAudio comes with 3 empty playlists named Attract, Home and IntroVideo. A real game would have one or more AudioClips in each playlist.
In the inspector, you specify the size of the playlist and that many Elements will appear. To assign an AudioClip to an Element:
When the P3SASceneController starts, it plays the playlist with the same name as the scene.
P3SAAudio.Instance.ChangePlaylistByName(sceneName); |
That starts the scene playlist indeed, but the watchdog has a bug, when its 2.5 second interval expires, it tries to update the playlist, doesn’t find anything applicable and it stops the scene playlist.
The PlaylistRequest feature is not used in P3SampleApp, but this is how it would work to play a playlist in a Mode. The requesting mode sends the Evt_RequestPlaylist event with a PlaylistRequest event data to add a playlist request. The PlaylistRequests are sorted and the mode with the highest priority has its playlist played. Do not forget to send Evt_RemovePlaylistRequests 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 remaining mode with the highest priority.
The default playlist from the scene controller is changed by name, it is not a PlaylistRequest. Makes sense since there is no requesting mode. If the default playlist is overridden by a PlaylistRequest, the only way to go back to the scene playlist is to call ChangePlaylistByName(sceneName), make an explicit PlaylistRequest for it, or wait for the game to eventually reload the scene.
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.
P3SAAudio plays the playlist on the MixerAudioGroup 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.
Here is a list of GUI components that call P3SAAudio.Instance.ChangePlaylistByName(). This has no effect in P3SampleApp because those Playlists don’t exist in P3SAAudio.
Component | Playlist |
Bonus | Bonus_Total |
Bonus | Bonus_Start |
IntroVideoSceneController | IntroAnimAudio |
P3SASceneController | BG_Mode_Introduction |
P3SASceneController | HighScoreEntry |
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 one clip will play depends on its weight compared to the other clips’ weight.
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 VariableAudioClip children that don’t 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:
Audio
GroupTest (AudioClipGroup)
GameObject (VariableAudioClip) for clip FX_AA_Hits_sm01 with some pitch range and FollowUpAudioClipGroup FollowupTest
GameObject (1) (VariableAudioClip) for clip FX_AA_Hits_sm03 with some pitch range
GameObject (2) (VariableAudioClip) for clip SoftMalletCymbalRoll
FollowupTest (AudioClipGroup) with sibling VariableAudioClip for clip FX_BallEjectToPlayer_
There are also multiple AudioClipGroups under TwitchEasterEggs in the Home scene.
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("GroupTest"); |
You will hear SoftMalletCymbalRoll more frequently because it has a weight of 5 compared to 1 for the other two clips. When FX_AA_Hits_sm01 plays, it is followed by FX_BallEjectToPlayer_ three seconds later.
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 AudioSoundGroup 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.