File: Internal\Synthesis\AudioDeviceOut.cs
Web Access
Project: src\src\runtime\src\libraries\System.Speech\src\System.Speech.csproj (System.Speech)
// 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
    }
}