Paradox Game Engine  v1.0.0 beta06
 All Classes Namespaces Files Functions Variables Typedefs Enumerations Enumerator Properties Events Macros Pages
DynamicSoundEffectInstance.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 using System;
4 using System.Threading;
5 using System.Threading.Tasks;
6 
7 using SiliconStudio.Paradox.Audio.Wave;
8 
9 namespace SiliconStudio.Paradox.Audio
10 {
11 
12  /// <summary>
13  /// <para>A dynamic SoundEffectInstance.</para>
14  /// <para>This class provides methods, properties and callbacks to enable SoundEffect wav data streaming and generation.
15  /// The user can choose its sound format when creating the <see cref="DynamicSoundEffectInstance"/></para>.
16  /// Then he can generate the audio data and submit it to the audio system.
17  /// The event <see cref="BufferNeeded"/> is called every time that sound need data.
18  /// </summary>
19  public sealed partial class DynamicSoundEffectInstance : SoundEffectInstance
20  {
21  internal object WorkerLock = new object();
22  internal bool IsDisposing;
23 
24  /// <summary>
25  /// The wave format of this dynamic sound effect.
26  /// </summary>
27  private readonly WaveFormat waveFormat;
28 
29  internal override WaveFormat WaveFormat
30  {
31  get { return waveFormat; }
32  }
33 
34  /// <summary>
35  /// This constant represent the number of buffers under which the <see cref="BufferNeeded"/> event should be thrown.
36  /// </summary>
37  private const int BufferNeededEventNbOfBuffers = 2;
38 
39  /// <summary>
40  /// Create a dynamic sound effect instance with the given sound properties.
41  /// </summary>
42  /// <param name="engine">The engine in which the dynamicSoundEffectInstance is created</param>
43  /// <param name="sampleRate">Sample rate, in Hertz (Hz), of audio content. Must between 8000 Hz and 48000 Hz</param>
44  /// <param name="channels">Number of channels in the audio data.</param>
45  /// <param name="encoding">Encoding of a sound data sample</param>
46  /// <returns>A new DynamicSoundEffectInstance instance ready to filled with data and then played</returns>
47  /// <exception cref="ArgumentOutOfRangeException">This exception is thrown for one of the following reason:
48  /// <list type="bullet">
49  /// <item>The value specified for sampleRate is less than 8000 Hz or greater than 48000 Hz. </item>
50  /// <item>The value specified for channels is something other than mono or stereo. </item>
51  /// <item>The value specified for data encoding is something other than 8 or 16 bits. </item>
52  /// </list>
53  /// </exception>
54  /// <exception cref="ArgumentNullException"><paramref name="engine"/> is null.</exception>
55  public DynamicSoundEffectInstance(AudioEngine engine, int sampleRate, AudioChannels channels, AudioDataEncoding encoding)
56  : base(engine)
57  {
58  if (engine == null)
59  throw new ArgumentNullException("engine");
60 
61  if (sampleRate < 8000 || 48000 < sampleRate)
62  throw new ArgumentOutOfRangeException("sampleRate");
63 
64  if(channels != AudioChannels.Mono && channels != AudioChannels.Stereo)
65  throw new ArgumentOutOfRangeException("channels");
66 
67  if(encoding != AudioDataEncoding.PCM_8Bits && encoding != AudioDataEncoding.PCM_16Bits)
68  throw new ArgumentOutOfRangeException("encoding");
69 
70  waveFormat = new WaveFormat(sampleRate, (int)encoding, (int)channels);
71 
72  Interlocked.Increment(ref totalNbOfInstances);
73  Interlocked.Increment(ref numberOfInstances);
74 
75  // first instance of dynamic sound effect instance => we create the workerThead and the associated event.
76  if (numberOfInstances == 1)
77  {
78  instancesNeedingBuffer = new ThreadSafeQueue<DynamicSoundEffectInstance>(); // to be sure that there is no remaining request from previous sessions
79  awakeWorkerThread = new AutoResetEvent(false);
80  CreateWorkerThread();
81  }
82 
83  Name = "Dynamic Sound Effect Instance - "+totalNbOfInstances;
84 
85  CreateVoice(WaveFormat);
86 
87  InitializeDynamicSound();
88 
89  AudioEngine.RegisterSound(this);
90 
91  ResetStateToDefault();
92  }
93 
94  /// <summary>
95  /// This represent the total number of instances created since the beginning of the game.
96  /// It is used the give a unique name to each Dynamic sound only.
97  /// </summary>
98  private static int totalNbOfInstances;
99 
100  /// <summary>
101  /// Submits an audio whole buffer for playback.
102  /// </summary>
103  /// <param name="buffer">Buffer that contains the audio data. The audio format must be PCM wave data.</param>
104  /// <remarks>
105  /// The buffer must conform to the format alignment. For example, the buffer length must be aligned to the block alignment value for the audio format type.
106  /// For PCM audio format, the block alignment is calculated as BlockAlignment = BytesPerSample * AudioChannels.
107  /// DynamicSoundEffectInstance supports only PCM 8bits and 16-bit mono or stereo data.
108  /// <para>Submited buffer must not be modified before it finished to play.</para>
109  /// <para>Scratches in the sound flow may appears if the submitted buffers are not big enough.</para>
110  /// </remarks>
111  /// <exception cref="ArgumentNullException"><paramref name="buffer"/> is null.</exception>
112  /// <exception cref="ObjectDisposedException">The exception thrown if SubmitBuffer is called after DynamicSoundEffectInstance is disposed.</exception>
113  /// <exception cref="ArgumentException">
114  /// The exception thrown when buffer is zero length, or does not satisfy format alignment restrictions.
115  /// </exception>
116  public void SubmitBuffer(byte[] buffer)
117  {
118  if (buffer == null)
119  throw new ArgumentNullException("buffer");
120 
121  SubmitBuffer(buffer, 0, buffer.Length);
122  }
123 
124  /// <summary>
125  /// Submits an audio buffer for playback. Playback begins at the specifed offset, and the byte count determines the size of the sample played.
126  /// </summary>
127  /// <param name="buffer">Buffer that contains the audio data. The audio format must be PCM wave data.</param>
128  /// <param name="offset">Offset, in bytes, to the starting position of the data.</param>
129  /// <param name="byteCount">Amount, in bytes, of data sent.</param>
130  /// <remarks>
131  /// The buffer must conform to the format alignment. For example, the buffer length must be aligned to the block alignment value for the audio format type.
132  /// For PCM audio format, the block alignment is calculated as BlockAlignment = BytesPerSample * AudioChannels.
133  /// DynamicSoundEffectInstance supports only PCM 8bits and 16-bit mono or stereo data.
134  /// <para>Submited buffer must not be modified before it finished to play.</para>
135  /// <para>Scratches in the sound flow may appears if the submitted buffers are not big enough.</para>
136  /// </remarks>
137  /// <exception cref="ObjectDisposedException">The exception thrown if SubmitBuffer is called after DynamicSoundEffectInstance is disposed.</exception>
138  /// <exception cref="ArgumentNullException"><paramref name="buffer"/> is null.</exception>
139  /// <exception cref="ArgumentException">
140  /// The exception thrown when <paramref name="buffer"/> is zero length, or <paramref name="byteCount"/> does not satisfy format alignment restrictions.
141  /// This exception also is thrown if offset is less than zero or is greater than or equal to the size of the buffer.
142  /// </exception>
143  /// <exception cref="ArgumentOutOfRangeException">
144  /// The exception thrown when <paramref name="byteCount"/>"/> is less than or equal to zero, or if the sum of <paramref name="offset"/> and <paramref name="byteCount"/> exceeds the size of the buffer.
145  /// </exception>
146  public void SubmitBuffer(byte[] buffer, int offset, int byteCount)
147  {
148  CheckNotDisposed();
149 
150  if(buffer == null)
151  throw new ArgumentNullException("buffer");
152 
153  if(buffer.Length == 0)
154  throw new ArgumentException("Buffer length is equal to zero.");
155 
156  if (byteCount % waveFormat.BlockAlign != 0)
157  throw new ArgumentException("The size of data to submit does not satisfy format alignment restrictions.");
158 
159  if (offset<0 || offset>=buffer.Length)
160  throw new ArgumentOutOfRangeException("offset");
161 
162  if(byteCount<0 || offset + byteCount > buffer.Length)
163  throw new ArgumentOutOfRangeException("byteCount");
164 
165  SubmitBufferImpl(buffer, offset, byteCount);
166 
167  CheckAndThrowBufferNeededEvent();
168  }
169 
170  /// <summary>
171  /// Returns the number of buffers that are waiting be to played by the audio system.
172  /// </summary>
173  /// <exception cref="ObjectDisposedException">The exception thrown if PendingBufferCount is called after DynamicSoundEffectInstance is disposed.</exception>
174  public int PendingBufferCount
175  {
176  get
177  {
178  CheckNotDisposed();
179 
180  return pendingBufferCount;
181  }
182  }
183  /// <summary>
184  /// Number of buffer waiting to be played from the user point of view.
185  /// </summary>
186  private int pendingBufferCount;
187 
188  /// <summary>
189  /// Number of buffer waiting to be played from the underlying implementation point of view.
190  /// </summary>
191  private int internalPendingBufferCount;
192 
193 
194  /// <summary>
195  /// Event that occurs when the sound instance is going to run out of audio data.
196  /// </summary>
197  /// <remarks>
198  /// More precisely, the event is thrown every time that:
199  /// <list type="bullet">
200  /// <item>the sound is playing and the number of buffers remaining to play is too low.</item>
201  /// <item>the number of buffers remaining after a <see cref="SubmitBuffer(byte[])"/> call is still not enough.</item>
202  /// </list>
203  /// </remarks>
204  public event EventHandler<EventArgs> BufferNeeded;
205 
206  /// <summary>
207  /// Indicates whether the audio playback of the <see cref="DynamicSoundEffectInstance"/> object is looped.
208  /// </summary>
209  /// <remarks>A sound cannot be looped with a <see cref="DynamicSoundEffectInstance"/>.
210  /// So accessing the property always returns false, and setting the property to true always throws the <see cref="InvalidOperationException"/> exception.</remarks>
211  /// <exception cref="InvalidOperationException">The exception that is thrown when IsLooped is set to true.</exception>
212  /// <exception cref="ObjectDisposedException">The exception that is thrown if IsLooped is called after <see cref="DynamicSoundEffectInstance"/> has been disposed. </exception>
213  public override bool IsLooped
214  {
215  get
216  {
217  return base.IsLooped;
218  }
219  set
220  {
221  CheckNotDisposed();
222 
223  if (value)
224  throw new InvalidOperationException("IsLooped can not be set to true with DynamicSoundEffectInstance.");
225  }
226  }
227 
228  internal override void ExitLoopImpl()
229  {
230  // there is nothing to do here since DynamicSoundEffectInstance can not be looped.
231  }
232 
233  public override void Play()
234  {
235  DataBufferLoaded = true;
236 
237  base.Play();
238 
239  CheckAndThrowBufferNeededEvent();
240  }
241 
242  /// <summary>
243  /// This method checks if the BufferNeeded event need to be thrown.
244  /// If it is the case, it invokes the methods associated to the event.
245  /// </summary>
246  private void CheckAndThrowBufferNeededEvent()
247  {
248  if (internalPendingBufferCount <= BufferNeededEventNbOfBuffers && BufferNeeded != null)
249  {
250  instancesNeedingBuffer.Enqueue(this);
251  awakeWorkerThread.Set();
252  }
253  }
254 
255  public override void Stop()
256  {
257  // submitted buffers need to be cleared even if the music is already stopped (-> overload stop instead of stopImpl)
258 
259  lock (WorkerLock) // we lock the worker thread here to avoid to have invalid states due to simultaneous Stop/Submit (via BufferNeeded callback).
260  {
261  base.Stop();
262 
263  pendingBufferCount = 0;
264  internalPendingBufferCount = 0;
265 
266  ClearBuffersImpl();
267 
268  lock (submittedBufferHandles.InternalLock)
269  {
270  foreach (var handles in submittedBufferHandles.InternalQueue)
271  handles.FreeHandles();
272 
273  submittedBufferHandles.InternalQueue.Clear();
274  }
275  }
276  }
277 
278  /// <summary>
279  /// The number of DynamicSoundEffectInstances.
280  /// It is used to determine when to create or destroy the working thread.
281  /// </summary>
282  private static int numberOfInstances;
283 
284  /// <summary>
285  /// Event used to awake the worker thread when there is some work to perform.
286  /// </summary>
287  private static AutoResetEvent awakeWorkerThread;
288 
289  /// <summary>
290  /// A reference to the worker task.
291  /// It is used to wait completion of the task before disposing the last instance of Dynamic sound.
292  /// </summary>
293  private static Task workerTask;
294 
295  /// <summary>
296  /// Queue of the instances that require a new buffer to be submitted.
297  /// It is used by the worker thread to determine which user callback to execute.
298  /// </summary>
299  private static ThreadSafeQueue<DynamicSoundEffectInstance> instancesNeedingBuffer;
300 
301  internal override void DestroyImpl()
302  {
303  AudioEngine.UnregisterSound(this);
304 
305  IsDisposing = true;
306 
307  lock (WorkerLock) // avoid to have simultaneous destroy and submit buffer (via BufferNeeded of working thread).
308  {
309  base.DestroyImpl();
310  }
311 
312  Interlocked.Decrement(ref numberOfInstances);
313 
314  if (numberOfInstances == 0)
315  {
316  awakeWorkerThread.Set();
317  if (!workerTask.Wait(500))
318  throw new AudioSystemInternalException("The DynamicSoundEffectInstance worker did not complete in allowed time.");
319  awakeWorkerThread.Dispose();
320  }
321  }
322 
323  /// <summary>
324  /// Create the working thread that will execute the user code on event <see cref="BufferNeeded"/> triggering.
325  /// </summary>
326  private static void CreateWorkerThread()
327  {
328  workerTask = Task.Factory.StartNew(WorkerThread);
329  }
330 
331  /// <summary>
332  /// The worker thread executing the user code on event <see cref="BufferNeeded"/>.
333  /// In current implementation the there is only one working thread for all the DynamicSoundEffectInstances.
334  /// </summary>
335  private static void WorkerThread()
336  {
337  while (true)
338  {
339  awakeWorkerThread.WaitOne();
340 
341  if (numberOfInstances == 0)
342  {
343  return;
344  }
345 
346  DynamicSoundEffectInstance instanceNeedingBuffer;
347  while (instancesNeedingBuffer.TryDequeue(out instanceNeedingBuffer))
348  {
349  lock (instanceNeedingBuffer.WorkerLock)
350  {
351  if (!instanceNeedingBuffer.IsDisposing && instanceNeedingBuffer.BufferNeeded != null)
352  {
353  instanceNeedingBuffer.BufferNeeded(instanceNeedingBuffer, EventArgs.Empty);
354  }
355  }
356  }
357  }
358  }
359 
360  /// <summary>
361  /// The queue of the handles to free as the buffers are consumed.
362  /// </summary>
363  private readonly ThreadSafeQueue<SubBufferDataHandles> submittedBufferHandles = new ThreadSafeQueue<SubBufferDataHandles>();
364 
365  private void OnBufferEndCommon()
366  {
367  SubBufferDataHandles bufferHandles;
368  if (submittedBufferHandles.TryDequeue(out bufferHandles))
369  {
370  Interlocked.Decrement(ref internalPendingBufferCount);
371  Interlocked.Add(ref pendingBufferCount, -bufferHandles.HandleCount);
372  bufferHandles.FreeHandles();
373  }
374 
375  CheckAndThrowBufferNeededEvent();
376  }
377  }
378 }
AudioDataEncoding
Enumeration describing the possible audio data encodings.
DynamicSoundEffectInstance(AudioEngine engine, int sampleRate, AudioChannels channels, AudioDataEncoding encoding)
Create a dynamic sound effect instance with the given sound properties.
override void Play()
Start or resume playing the sound.
Represents the audio engine. In current version, the audio engine necessarily creates its context on ...
Definition: AudioEngine.cs:80
void SubmitBuffer(byte[] buffer, int offset, int byteCount)
Submits an audio buffer for playback. Playback begins at the specifed offset, and the byte count dete...
void SubmitBuffer(byte[] buffer)
Submits an audio whole buffer for playback.
EventHandler< EventArgs > BufferNeeded
Event that occurs when the sound instance is going to run out of audio data.
override void Stop()
Stop playing the sound immediately and reset the sound to the beginning of the track.
Instance of a SoundEffect sound which can be independently localized and played.
AudioChannels
Enumeration containing the different audio output configurations.