Paradox Game Engine  v1.0.0 beta06
 All Classes Namespaces Files Functions Variables Typedefs Enumerations Enumerator Properties Events Macros Pages
AudioEngine.cs
Go to the documentation of this file.
1 // Copyright (c) 2014 Silicon Studio Corp. (http://siliconstudio.co.jp)
2 // This file is distributed under GPL v3. See LICENSE.md for details.
3 
4 using System;
5 using System.Linq;
6 using System.Collections.Generic;
7 
8 using SiliconStudio.Core;
9 using SiliconStudio.Core.Diagnostics;
10 
11 namespace SiliconStudio.Paradox.Audio
12 {
13 
14  /// <summary>
15  /// Describe an play action that a SoundMusic can request to the AudioEngine
16  /// </summary>
17  internal enum SoundMusicAction
18  {
19  Play,
20  Pause,
21  Stop,
22  Volume,
23  }
24 
25  /// <summary>
26  /// A SoundMusic action request aimed for the AudioEngine.
27  /// </summary>
28  internal struct SoundMusicActionRequest
29  {
30  public SoundMusic Requester;
31 
32  public SoundMusicAction RequestedAction;
33 
34  public SoundMusicActionRequest(SoundMusic requester, SoundMusicAction request)
35  {
36  Requester = requester;
37  RequestedAction = request;
38  }
39  }
40 
41  /// <summary>
42  /// A <see cref="SoundMusic"/> event.
43  /// </summary>
44  internal enum SoundMusicEvent
45  {
51  }
52 
53  /// <summary>
54  /// A notification of an SoundMusic event.
55  /// </summary>
56  internal struct SoundMusicEventNotification
57  {
58  public SoundMusicEvent Event;
59 
60  public object EventData;
61 
62  public SoundMusicEventNotification(SoundMusicEvent mEvent, object eventData)
63  {
64  Event = mEvent;
65  EventData = eventData;
66  }
67  }
68 
69  /// <summary>
70  /// Represents the audio engine.
71  /// In current version, the audio engine necessarily creates its context on the default audio hardware of the device.
72  /// The audio engine is required when creating or loading sounds.
73  /// </summary>
74  /// <remarks>The AudioEngine is Disposable. Call the <see cref="ComponentBase.Dispose"/> function when you do not need to play sounds anymore to free memory allocated to the audio system.
75  /// A call to Dispose automatically stops and disposes all the <see cref="SoundEffect"/>, <see cref="SoundEffectInstance"/> and <see cref="DynamicSoundEffectInstance"/>
76  /// sounds created by this AudioEngine</remarks>
77  /// <seealso cref="SoundEffect.Load"/>
78  /// <seealso cref="SoundMusic.Load"/>
79  /// <seealso cref="DynamicSoundEffectInstance"/>
80  public sealed partial class AudioEngine : ComponentBase
81  {
82  /// <summary>
83  /// The logger of the audio engine.
84  /// </summary>
85  public static readonly Logger Logger = GlobalLogger.GetLogger("AudioEngine");
86 
87  /// <summary>
88  /// Create an Audio Engine on the default audio device
89  /// </summary>
90  /// <param name="sampleRate">The desired sample rate of the audio graph. 0 let the engine choose the best value depending on the hardware.</param>
91  /// <exception cref="AudioInitializationException">Initialization of the audio engine failed. May be due to memory problems or missing audio hardware.</exception>
92  public AudioEngine(uint sampleRate = 0)
93  : this(null, sampleRate)
94  {
95  }
96 
97  /// <summary>
98  /// Create the Audio Engine on the specified device.
99  /// </summary>
100  /// <param name="device">Device on which to create the audio engine.</param>
101  /// <param name="sampleRate">The desired sample rate of the audio graph. 0 let the engine choose the best value depending on the hardware.</param>
102  /// <remarks>Available devices can be queried by calling static method <see cref="GetAvailableDevices"/></remarks>
103  /// <exception cref="AudioInitializationException">Initialization of the audio engine failed. May be due to memory problems or missing audio hardware.</exception>
104  private AudioEngine(AudioDevice device, uint sampleRate = 0)
105  {
106  if (device != null)
107  throw new NotImplementedException();
108 
109  State = AudioEngineState.Running;
110 
111  AudioSampleRate = sampleRate;
112 
113  AudioEngineImpl(device);
114 
115  ++nbOfAudioEngineInstances;
116  }
117 
118  /// <summary>
119  /// Get the list of the audio hardware available on the device.
120  /// </summary>
121  /// <returns>List of available devices</returns>
122  /// <seealso cref="AudioEngine"/>
123  private static List<AudioDevice> GetAvailableDevices()
124  {
125  throw new NotImplementedException();
126  }
127 
128  private static int nbOfAudioEngineInstances;
129 
130  /// <summary>
131  /// The list of the sounds that have been paused by the call to <see cref="PauseAudio"/> and should be resumed by <see cref="ResumeAudio"/>.
132  /// </summary>
133  private readonly List<SoundInstanceBase> pausedSounds = new List<SoundInstanceBase>();
134 
135  /// <summary>
136  /// The underlying sample rate of the audio system.
137  /// </summary>
138  internal uint AudioSampleRate { get; private set; }
139 
140  /// <summary>
141  /// Method that updates all the sounds play status.
142  /// </summary>
143  /// <remarks>Should be called in same thread as user main thread.</remarks>
144  /// <exception cref="InvalidOperationException">One or several of the sounds asked for play had invalid data (corrupted or unsupported formats).</exception>
145  public void Update()
146  {
147  UpdateMusicImpl();
148  }
149 
150  /// <summary>
151  /// The current state of the <see cref="AudioEngine"/>.
152  /// </summary>
153  public AudioEngineState State { get; private set; }
154 
155  /// <summary>
156  /// Pause the audio engine. That is, pause all the currently playing <see cref="SoundInstanceBase"/>, and block any future play until <see cref="ResumeAudio"/> is called.
157  /// </summary>
158  public void PauseAudio()
159  {
160  if(State != AudioEngineState.Running)
161  return;
162 
163  State = AudioEngineState.Paused;
164 
165  PauseAudioPlatformSpecific();
166 
167  pausedSounds.Clear();
168  foreach (var sound in notDisposedSounds)
169  {
170  var soundEffect = sound as SoundEffect;
171  if (soundEffect != null)
172  {
173  foreach (var instance in soundEffect.Instances)
174  {
175  if (instance.PlayState == SoundPlayState.Playing)
176  {
177  instance.Pause();
178  pausedSounds.Add(instance);
179  }
180  }
181  }
182  var soundInstance = sound as SoundInstanceBase;
183  if(soundInstance != null && soundInstance.PlayState == SoundPlayState.Playing)
184  {
185  soundInstance.Pause();
186  pausedSounds.Add(soundInstance);
187  }
188  }
189  }
190 
191  /// <summary>
192  /// Resume all audio engine. That is, resume the sounds paused by <see cref="PauseAudio"/>, and re-authorize future calls to play.
193  /// </summary>
194  public void ResumeAudio()
195  {
196  if(State != AudioEngineState.Paused)
197  return;
198 
199  State = AudioEngineState.Running;
200 
201  ResumeAudioPlatformSpecific();
202 
203  foreach (var playableSound in pausedSounds)
204  {
205  if (!playableSound.IsDisposed && playableSound.PlayState == SoundPlayState.Paused) // sounds can have been stopped by user while the audio engine was paused.
206  playableSound.Play();
207  }
208  }
209 
210  /// <summary>
211  /// Return the <see cref="SoundEffect"/> evaluated as having least impact onto the final audio output among all the non stopped <see cref="SoundEffect"/>.
212  /// </summary>
213  /// <returns>An instance of <see cref="SoundEffect"/> currently playing or paused, or null if no candidate</returns>
215  {
216  // the current heuristic consist in finding the shortest element not looped
217  SoundEffect bestCandidate = null;
218  var bestCandidateSize = int.MaxValue;
219  lock (notDisposedSounds)
220  {
221  foreach (var notDisposedSound in notDisposedSounds)
222  {
223  var soundEffect = notDisposedSound as SoundEffect;
224  if(soundEffect == null)
225  continue;
226 
227  var sizeSoundEffect = soundEffect.WaveDataSize / soundEffect.WaveFormat.BlockAlign;
228  if(sizeSoundEffect >= bestCandidateSize)
229  continue;
230 
231  foreach (var instance in soundEffect.Instances)
232  {
233  if (instance.PlayState != SoundPlayState.Stopped && !instance.IsLooped)
234  {
235  bestCandidate = soundEffect;
236  bestCandidateSize = sizeSoundEffect;
237  break;
238  }
239  }
240 
241  }
242  }
243 
244  return bestCandidate;
245  }
246 
247  /// <summary>
248  /// Force all the <see cref="SoundEffectInstance"/> to update them-self
249  /// </summary>
250  internal void ForceSoundEffectInstanceUpdate()
251  {
252  lock (notDisposedSounds)
253  {
254  foreach (var notDisposedSound in notDisposedSounds)
255  {
256  var soundEffect = notDisposedSound as SoundEffect;
257  if (soundEffect == null)
258  continue;
259 
260  foreach (var instance in soundEffect.Instances)
261  instance.PlayState = instance.PlayState;
262  }
263  }
264  }
265 
266  private readonly List<SoundBase> notDisposedSounds = new List<SoundBase>();
267 
268  internal void RegisterSound(SoundBase newSound)
269  {
270  lock (notDisposedSounds)
271  {
272  notDisposedSounds.Add(newSound);
273  }
274  }
275 
276  internal void UnregisterSound(SoundBase disposedSound)
277  {
278  lock (notDisposedSounds)
279  {
280  if (!notDisposedSounds.Remove(disposedSound))
281  throw new AudioSystemInternalException("Try to remove a disposed sound not in the list of registered sounds.");
282  }
283  }
284 
285  protected override void Destroy()
286  {
287  base.Destroy();
288 
289  if (IsDisposed)
290  return;
291 
292  State = AudioEngineState.Disposed;
293 
294  SoundBase[] notDisposedSoundsArray;
295  lock (notDisposedSounds)
296  {
297  notDisposedSoundsArray = notDisposedSounds.ToArray();
298  }
299 
300  // Dispose all the sound not disposed yet.
301  foreach (var soundBase in notDisposedSoundsArray)
302  soundBase.Dispose();
303 
304  --nbOfAudioEngineInstances;
305 
306  DestroyImpl();
307  }
308 
309  /// <summary>
310  /// The pending Sound Music requests
311  /// </summary>
312  private readonly ThreadSafeQueue<SoundMusicActionRequest> musicActionRequests = new ThreadSafeQueue<SoundMusicActionRequest>();
313 
314  /// <summary>
315  /// Submit an new MusicAction request to the AudioEngine. It will effectively executed during the next AudioEngine.Update().
316  /// </summary>
317  /// <param name="request">The request to submit.</param>
318  internal void SubmitMusicActionRequest(SoundMusicActionRequest request)
319  {
320  musicActionRequests.Enqueue(request);
321  }
322 
323  /// <summary>
324  /// The music that is currently playing or the last music that have been played.
325  /// </summary>
326  private SoundMusic currentMusic;
327 
328  private void QueueLastPendingRequests(Dictionary<SoundMusicAction, bool> requestAftPlay, SoundMusic requester)
329  {
330  lock (musicActionRequests)
331  {
332  foreach (var action in requestAftPlay.Where(x => x.Value).Select(x => x.Key))
333  musicActionRequests.Enqueue(new SoundMusicActionRequest(requester, action));
334  }
335  }
336 
337  /// <summary>
338  /// This method process all the events from the MusicPlayer and the requests from the SoundMusics.
339  /// <para>There is only a single instance MusicPlayer for several SoundMusic play requests, that why we need to change the player data.
340  /// That is why we need one or more call to <see cref="Update"/> to change the current music.</para>
341  /// <para>In order to avoid useless change of the MusicPlayer data,
342  /// we analyse the whole MusicActionRequest queue and drop all the requests that are followed by subsequent Play-Requests.</para>
343  /// </summary>
344  private void UpdateMusicImpl()
345  {
346  // 1. First deals with the events from the media Session received since the last update
347  ProccessQueuedMediaSessionEvents();
348 
349  // 2. Deals with the SoundMusic action requests
350  ProcessMusicActionRequests();
351  }
352 
353  /// <summary>
354  /// In order to avoid problem that appeared (see TestMediaFoundation) when using MediaSession.SetTopology,
355  /// we destroy and recreate the media session every time we change the music to play.
356  /// Before shutting down the MediaSession we need to receive the SessionClosed.
357  /// </summary>
358  private void ProcessMusicActionRequests()
359  {
360  // Here we need to analyse all the request flow to determine which action really need to be performed at the end.
361 
362  // skip everythings if no requests
363  if (musicActionRequests.Count == 0)
364  return;
365 
366  // postpone the proccess of the requests to next Update if there exist a media session but it is not ready yet to be played.
367  if (currentMusic != null && !isMusicPlayerReady)
368  return;
369 
370  // now analyse the list of requests to determine the actions to performs
371  var lastPlayRequestMusicInstance = currentMusic; // the last SoundMusic instance that asked for a Play-Request.
372  var actionRequestedAftLastPlay = new Dictionary<SoundMusicAction, bool> // Specify if there is Request to the given Action after the Play-Request of the last played music.
373  {
374  { SoundMusicAction.Pause, false },
375  { SoundMusicAction.Stop, false },
376  { SoundMusicAction.Volume, false }
377  };
378  var shouldRestartCurrentMusic = false;
379  var shouldStartCurrentMusic = false;
380 
381  foreach (var actionRequest in musicActionRequests.DequeueAsList())
382  {
383  if (actionRequest.RequestedAction == SoundMusicAction.Play)
384  {
385  lastPlayRequestMusicInstance = actionRequest.Requester;
386  shouldRestartCurrentMusic = shouldRestartCurrentMusic || lastPlayRequestMusicInstance != currentMusic || actionRequestedAftLastPlay[SoundMusicAction.Stop];
387  shouldStartCurrentMusic = true;
388  foreach (var action in actionRequestedAftLastPlay.Keys.ToArray())
389  actionRequestedAftLastPlay[action] = false;
390  }
391  else if (actionRequest.Requester == lastPlayRequestMusicInstance)
392  {
393  actionRequestedAftLastPlay[actionRequest.RequestedAction] = true;
394  }
395  }
396 
397  // perform the action depending on the request analysis.
398  if (currentMusic == null)
399  {
400  // there is currently no music loaded.
401  // => just load the last music to play and queue subsequent pause/stop/volume requests
402 
403  // no play requests => nothing to do (just skip everything)
404  if (lastPlayRequestMusicInstance == null)
405  return;
406 
407  // Load the music corresponding to the last play-request.
408  if(!lastPlayRequestMusicInstance.IsDisposed) // music can be disposed by the user before the call to Update.
409  LoadNewMusic(lastPlayRequestMusicInstance);
410 
411  // Queue the remaining waiting requests
412  QueueLastPendingRequests(actionRequestedAftLastPlay, currentMusic);
413  }
414  else
415  {
416  // there is currently one music loaded:
417  // - if another music need to be played => close the session and queue Play/Pause/Stop/Volume requests
418  // - else => perform Play/Pause/Stop/Volume request on the current music.
419 
420  if (currentMusic != lastPlayRequestMusicInstance) // need to change the music
421  {
422  ResetMusicPlayer();
423  musicActionRequests.Enqueue(new SoundMusicActionRequest(lastPlayRequestMusicInstance, SoundMusicAction.Play));
424  QueueLastPendingRequests(actionRequestedAftLastPlay, lastPlayRequestMusicInstance);
425  }
426  else if(!currentMusic.IsDisposed)// apply requests on current music.
427  {
428  if (shouldRestartCurrentMusic)
429  RestartCurrentMusic();
430 
431  if (shouldStartCurrentMusic)
432  StartCurrentMusic();
433 
434  if (actionRequestedAftLastPlay[SoundMusicAction.Volume])
435  UpdateMusicVolume();
436 
437  if (actionRequestedAftLastPlay[SoundMusicAction.Stop])
438  StopCurrentMusic();
439  else if (actionRequestedAftLastPlay[SoundMusicAction.Pause])
440  PauseCurrentMusic();
441  }
442  else // current music has been disposed.
443  {
444  ResetMusicPlayer();
445  }
446  }
447  }
448 
449  private void ProcessMusicEnded()
450  {
451  if (currentMusic == null || currentMusic.IsDisposed) //this event is asynchronous so the music can be disposed by the user meanwhile.
452  return;
453 
454  // If the music need to be looped, we start it again.
455  if (currentMusic.IsLooped && !currentMusic.ShouldExitLoop && currentMusic.PlayState != SoundPlayState.Stopped)
456  {
457  RestartCurrentMusic(); // restart the sound to simulate looping.
458  StartCurrentMusic();
459  }
460  // otherwise we set the state of currentMusic to "stopped"
461  else
462  {
463  currentMusic.SetStateToStopped();
464  }
465  }
466 
467  private void ProcessMusicReady()
468  {
469  PlatformSpecificProcessMusicReady();
470 
471  isMusicPlayerReady = true;
472 
473  if (!currentMusic.IsDisposed) // disposal of the music can happen between the call to Play and its ready-to-play state notification
474  {
475  // Adjust the volume before starting to play.
476  UpdateMusicVolume();
477 
478  // Play the music
479  StartCurrentMusic();
480  }
481  }
482 
483  /// <summary>
484  /// The music MediaEvents that arrrived since last update.
485  /// </summary>
486  private readonly ThreadSafeQueue<SoundMusicEventNotification> musicMediaEvents = new ThreadSafeQueue<SoundMusicEventNotification>();
487 
488  private bool isMusicPlayerReady;
489 
490  private void ProccessQueuedMediaSessionEvents()
491  {
492  foreach (var eventNotification in musicMediaEvents.DequeueAsList())
493  {
494  // Since player calls are asynchronous, error status come asynchronously too
495  // We need to check here that the cmd executed properly.
496  ProcessMusicError(eventNotification);
497 
498  switch (eventNotification.Event)
499  {
500  case SoundMusicEvent.MetaDataLoaded:
501  ProcessMusicMetaData();
502  break;
503  case SoundMusicEvent.ReadyToBePlayed:
504  ProcessMusicReady();
505  break;
506  case SoundMusicEvent.EndOfTrackReached:
507  ProcessMusicEnded();
508  break;
509  case SoundMusicEvent.MusicPlayerClosed:
510  ProcessPlayerClosed();
511  break;
512  }
513 
514  // Release the event data if needed
515  var dispEventData = eventNotification.EventData as IDisposable;
516  if (dispEventData != null)
517  dispEventData.Dispose();
518  }
519  }
520  }
521 }
void Update()
Method that updates all the sounds play status.
Definition: AudioEngine.cs:145
AudioEngineState
Describe the possible states of the AudioEngine.
void PauseAudio()
Pause the audio engine. That is, pause all the currently playing SoundInstanceBase, and block any future play until ResumeAudio is called.
Definition: AudioEngine.cs:158
This class provides a loaded sound resource which is localizable in the 3D scene. ...
Definition: SoundEffect.cs:46
SoundPlayState
Current state (playing, paused, or stopped) of a sound implementing the IPlayableSound interface...
void ResumeAudio()
Resume all audio engine. That is, resume the sounds paused by PauseAudio, and re-authorize future cal...
Definition: AudioEngine.cs:194
SoundEffect GetLeastSignificativeSoundEffect()
Return the SoundEffect evaluated as having least impact onto the final audio output among all the non...
Definition: AudioEngine.cs:214
Base class for a framework component.
Base class for sound that creates voices
Base implementation for ILogger.
Definition: Logger.cs:10
Represents the audio engine. In current version, the audio engine necessarily creates its context on ...
Definition: AudioEngine.cs:80
Base class for all the sounds and sound instances.
Definition: SoundBase.cs:15
AudioEngine(uint sampleRate=0)
Create an Audio Engine on the default audio device
Definition: AudioEngine.cs:92
override void Destroy()
Disposes of object resources.
Definition: AudioEngine.cs:285