|
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
using System.Collections.Generic;
using System.Runtime.InteropServices;
using System.Threading;
namespace System.Speech.Internal.Synthesis
{
/// <summary>
/// Encapsulates Waveform Audio Interface playback functions and provides a simple
/// interface for playing audio.
/// </summary>
internal class AudioDeviceOut : AudioBase, IDisposable
{
#region Constructors
/// <summary>
/// Create an instance of AudioDeviceOut.
/// </summary>
internal AudioDeviceOut(int curDevice, IAsyncDispatch asyncDispatch)
{
_delegate = new Interop.WinMM.WaveOutProc(CallBackProc);
_asyncDispatch = asyncDispatch;
_curDevice = curDevice;
}
~AudioDeviceOut()
{
Dispose(false);
}
public void Dispose()
{
Dispose(true);
GC.SuppressFinalize(this);
}
private void Dispose(bool disposing)
{
if (_deviceOpen && _hwo != IntPtr.Zero)
{
Interop.WinMM.waveOutClose(_hwo);
_deviceOpen = false;
}
if (disposing)
{
((IDisposable)_evt).Dispose();
}
}
#endregion
#region Internal Methods
#region AudioDevice implementation
/// <summary>
/// Begin to play
/// </summary>
internal override void Begin(byte[] wfx)
{
if (_deviceOpen)
{
System.Diagnostics.Debug.Assert(false);
throw new InvalidOperationException();
}
// Get the alignments values
WAVEFORMATEX.AvgBytesPerSec(wfx, out _nAvgBytesPerSec, out _blockAlign);
Interop.WinMM.MMSYSERR result;
lock (_noWriteOutLock)
{
result = Interop.WinMM.waveOutOpen(ref _hwo, _curDevice, wfx, _delegate, IntPtr.Zero, Interop.WinMM.CALLBACK_FUNCTION);
if (_fPaused && result == Interop.WinMM.MMSYSERR.NOERROR)
{
result = Interop.WinMM.waveOutPause(_hwo);
}
// set the flags
_aborted = false;
_deviceOpen = true;
}
if (result != Interop.WinMM.MMSYSERR.NOERROR)
{
throw new AudioException(result);
}
// Reset the counter for the number of bytes written so far
_bytesWritten = 0;
// Nothing in the queue
_evt.Set();
}
/// <summary>
/// Begin to play
/// </summary>
internal override void End()
{
if (!_deviceOpen)
{
System.Diagnostics.Debug.Assert(false);
throw new InvalidOperationException();
}
lock (_noWriteOutLock)
{
_deviceOpen = false;
CheckForAbort();
if (_queueIn.Count != 0)
{
Interop.WinMM.waveOutReset(_hwo);
}
// Close it; no point in returning errors if this fails
Interop.WinMM.MMSYSERR result = Interop.WinMM.waveOutClose(_hwo);
if (result != Interop.WinMM.MMSYSERR.NOERROR)
{
// This may create a dead lock
System.Diagnostics.Debug.Assert(false);
}
}
}
/// <summary>
/// Play a wave file.
/// </summary>
internal override void Play(byte[] buffer)
{
if (!_deviceOpen)
{
System.Diagnostics.Debug.Assert(false);
}
else
{
int bufferSize = buffer.Length;
_bytesWritten += bufferSize;
System.Diagnostics.Debug.Assert(bufferSize % _blockAlign == 0);
WaveHeader waveHeader = new(buffer);
GCHandle waveHdr = waveHeader.WAVEHDR;
Interop.WinMM.MMSYSERR result = Interop.WinMM.waveOutPrepareHeader(_hwo, waveHdr.AddrOfPinnedObject(), waveHeader.SizeHDR);
if (result != Interop.WinMM.MMSYSERR.NOERROR)
{
throw new AudioException(result);
}
lock (_noWriteOutLock)
{
if (!_aborted)
{
lock (_queueIn)
{
InItem item = new(waveHeader);
_queueIn.Add(item);
// Something in the queue cannot exit anymore
_evt.Reset();
}
// Start playback of the first buffer
result = Interop.WinMM.waveOutWrite(_hwo, waveHdr.AddrOfPinnedObject(), waveHeader.SizeHDR);
if (result != Interop.WinMM.MMSYSERR.NOERROR)
{
lock (_queueIn)
{
_queueIn.RemoveAt(_queueIn.Count - 1);
throw new AudioException(result);
}
}
}
}
}
}
/// <summary>
/// Pause the playback of a sound.
/// </summary>
internal override void Pause()
{
lock (_noWriteOutLock)
{
if (!_aborted && !_fPaused)
{
if (_deviceOpen)
{
Interop.WinMM.MMSYSERR result = Interop.WinMM.waveOutPause(_hwo);
if (result != Interop.WinMM.MMSYSERR.NOERROR)
{
System.Diagnostics.Debug.Fail(((int)result).ToString(System.Globalization.CultureInfo.InvariantCulture));
}
}
_fPaused = true;
}
}
}
/// <summary>
/// Resume the playback of a paused sound.
/// </summary>
internal override void Resume()
{
lock (_noWriteOutLock)
{
if (!_aborted && _fPaused)
{
if (_deviceOpen)
{
Interop.WinMM.MMSYSERR result = Interop.WinMM.waveOutRestart(_hwo);
if (result != Interop.WinMM.MMSYSERR.NOERROR)
{
System.Diagnostics.Debug.Assert(false);
}
}
}
}
_fPaused = false;
}
/// <summary>
/// Wait for all the queued buffers to be played
/// </summary>
internal override void Abort()
{
lock (_noWriteOutLock)
{
_aborted = true;
if (_queueIn.Count > 0)
{
Interop.WinMM.waveOutReset(_hwo);
_evt.WaitOne();
}
}
}
internal override void InjectEvent(TTSEvent ttsEvent)
{
if (_asyncDispatch != null && !_aborted)
{
lock (_queueIn)
{
// Throw immediately if the queue is empty
if (_queueIn.Count == 0)
{
_asyncDispatch.Post(ttsEvent);
}
else
{
// Will be thrown before the next write to the audio device
_queueIn.Add(new InItem(ttsEvent));
}
}
}
}
/// <summary>
/// Wait for all the queued buffers to be played
/// </summary>
internal override void WaitUntilDone()
{
if (!_deviceOpen)
{
System.Diagnostics.Debug.Assert(false);
throw new InvalidOperationException();
}
_evt.WaitOne();
}
#endregion
#region Audio device specific methods
/// <summary>
/// Determine the number of available playback devices.
/// </summary>
/// <returns>Number of output devices</returns>
internal static int NumDevices()
{
return Interop.WinMM.waveOutGetNumDevs();
}
internal static int GetDevicedId(string name)
{
for (int iDevice = 0; iDevice < NumDevices(); iDevice++)
{
string device;
if (GetDeviceName(iDevice, out device) == Interop.WinMM.MMSYSERR.NOERROR && string.Equals(device, name, StringComparison.OrdinalIgnoreCase))
{
return iDevice;
}
}
return -1;
}
/// <summary>
/// Get the name of the specified playback device.
/// </summary>
/// <param name="deviceId">ID of the device</param>
/// <param name="prodName">Destination string assigned the name</param>
/// <returns>MMSYSERR.NOERROR if successful</returns>
internal static Interop.WinMM.MMSYSERR GetDeviceName(int deviceId, [MarshalAs(UnmanagedType.LPWStr)] out string prodName)
{
prodName = string.Empty;
Interop.WinMM.WAVEOUTCAPS caps = new();
Interop.WinMM.MMSYSERR result = Interop.WinMM.waveOutGetDevCaps((IntPtr)deviceId, ref caps, Marshal.SizeOf<Interop.WinMM.WAVEOUTCAPS>());
if (result != Interop.WinMM.MMSYSERR.NOERROR)
{
return result;
}
prodName = caps.szPname;
return Interop.WinMM.MMSYSERR.NOERROR;
}
#endregion
#endregion
#region Internal Fields
internal override TimeSpan Duration
{
get
{
if (_nAvgBytesPerSec == 0)
{
return new TimeSpan(0);
}
return new TimeSpan((_bytesWritten * TimeSpan.TicksPerSecond) / _nAvgBytesPerSec);
}
}
#endregion
#region Private Methods
private void CallBackProc(IntPtr hwo, Interop.WinMM.MM_MSG uMsg, IntPtr dwInstance, IntPtr dwParam1, IntPtr dwParam2)
{
if (uMsg == Interop.WinMM.MM_MSG.MM_WOM_DONE)
{
InItem inItem;
lock (_queueIn)
{
inItem = _queueIn[0];
inItem.ReleaseData();
_queueIn.RemoveAt(0);
_queueOut.Add(inItem);
// look for the next elements in the queue if they are events to throw!
while (_queueIn.Count > 0)
{
inItem = _queueIn[0];
// Do we have an event or a sound buffer
if (inItem._waveHeader == null)
{
if (_asyncDispatch != null && !_aborted)
{
_asyncDispatch.Post(inItem._userData!);
}
_queueIn.RemoveAt(0);
}
else
{
break;
}
}
}
// if the queue is empty, then restart the callers thread
if (_queueIn.Count == 0)
{
_evt.Set();
}
}
}
private void ClearBuffers()
{
foreach (InItem item in _queueOut)
{
WaveHeader waveHeader = item._waveHeader!;
Interop.WinMM.MMSYSERR result = Interop.WinMM.waveOutUnprepareHeader(
_hwo, waveHeader.WAVEHDR.AddrOfPinnedObject(), waveHeader.SizeHDR);
if (result != Interop.WinMM.MMSYSERR.NOERROR)
{
//System.Diagnostics.Debug.Assert (false);
}
waveHeader.Dispose();
}
_queueOut.Clear();
}
private void CheckForAbort()
{
if (_aborted)
{
// Synchronous operation
lock (_queueIn)
{
foreach (InItem inItem in _queueIn)
{
// Do we have an event or a sound buffer
if (inItem._waveHeader != null)
{
WaveHeader waveHeader = inItem._waveHeader;
Interop.WinMM.waveOutUnprepareHeader(
_hwo, waveHeader.WAVEHDR.AddrOfPinnedObject(), waveHeader.SizeHDR);
waveHeader.Dispose();
}
else
{
_asyncDispatch.Post(inItem._userData!);
}
}
_queueIn.Clear();
// if the queue is empty, then restart the callers thread
_evt.Set();
}
}
ClearBuffers();
}
#endregion
#region Private Types
/// <summary>
/// This object must keep a reference to the waveHeader object
/// so that the pinned buffer containing the data is not
/// released before it is finished being played
/// </summary>
private sealed class InItem : IDisposable
{
internal InItem(WaveHeader waveHeader)
{
_waveHeader = waveHeader;
}
internal InItem(TTSEvent userData)
{
_userData = userData;
}
public void Dispose()
{
_waveHeader?.Dispose();
GC.SuppressFinalize(this);
}
internal void ReleaseData()
{
_waveHeader?.ReleaseData();
}
internal WaveHeader? _waveHeader;
internal object? _userData;
}
#endregion
#region Private Fields
private List<InItem> _queueIn = new();
private List<InItem> _queueOut = new();
private int _blockAlign;
private int _bytesWritten;
private int _nAvgBytesPerSec;
private IntPtr _hwo;
private int _curDevice;
private ManualResetEvent _evt = new(false);
private Interop.WinMM.WaveOutProc _delegate;
private IAsyncDispatch _asyncDispatch;
private bool _deviceOpen;
private object _noWriteOutLock = new();
private bool _fPaused;
#endregion
}
}
|