File: Internal\Synthesis\EngineSiteSapi.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.Diagnostics.CodeAnalysis;
using System.IO;
using System.Runtime.InteropServices;
using System.Runtime.InteropServices.ComTypes;
using System.Speech.Internal.SapiInterop;
using System.Speech.Synthesis.TtsEngine;

namespace System.Speech.Internal.Synthesis
{
    [ComVisible(true)]
    internal class EngineSiteSapi : ISpEngineSite
    {
        #region Constructors

        internal EngineSiteSapi(EngineSite site, ResourceLoader resourceLoader)
        {
            _site = site;
        }

        #endregion

        #region Internal Methods

        /// <summary>
        /// Adds events directly to an event sink.
        /// </summary>
        void ISpEngineSite.AddEvents([MarshalAs(UnmanagedType.LPArray, SizeParamIndex = 1)] SpeechEventSapi[] eventsSapi, int ulCount)
        {
            SpeechEventInfo[] events = new SpeechEventInfo[eventsSapi.Length];
            for (int i = 0; i < eventsSapi.Length; i++)
            {
                SpeechEventSapi sapiEvt = eventsSapi[i];
                events[i].EventId = sapiEvt.EventId;
                events[i].ParameterType = sapiEvt.ParameterType;
                events[i].Param1 = (int)sapiEvt.Param1;
                events[i].Param2 = sapiEvt.Param2;
            }
            _site.AddEvents(events, ulCount);
        }

        /// <summary>
        /// Passes back the event interest for the voice.
        /// </summary>
        void ISpEngineSite.GetEventInterest(out long eventInterest)
        {
            eventInterest = (uint)_site.EventInterest;
        }

        /// <summary>
        ///  Queries the voice object to determine which real-time action(s) to perform
        /// </summary>
        [PreserveSig]
        int ISpEngineSite.GetActions()
        {
            return _site.Actions;
        }

        /// <summary>
        /// Queries the voice object to determine which real-time action(s) to perform.
        /// </summary>
        void ISpEngineSite.Write(IntPtr pBuff, int cb, IntPtr pcbWritten)
        {
            pcbWritten = (IntPtr)_site.Write(pBuff, cb);
        }

        /// <summary>
        ///  Retrieves the current TTS rendering rate adjustment that should be used by the engine.
        /// </summary>
        void ISpEngineSite.GetRate(out int pRateAdjust)
        {
            pRateAdjust = _site.Rate;
        }

        /// <summary>
        /// Retrieves the base output volume level the engine should use during synthesis.
        /// </summary>
        void ISpEngineSite.GetVolume(out short pusVolume)
        {
            pusVolume = (short)_site.Volume;
        }

        /// <summary>
        /// Retrieves the number and type of items to be skipped in the text stream.
        /// </summary>
        void ISpEngineSite.GetSkipInfo(out int peType, out int plNumItems)
        {
            SkipInfo si = _site.GetSkipInfo();
            if (si != null)
            {
                peType = si.Type;
                plNumItems = si.Count;
            }
            else
            {
                peType = 1; // BSPVSKIPTYPE.SPVST_SENTENCE;
                plNumItems = 0;
            }
        }

        /// <summary>
        /// Notifies that the last skip request has been completed and to pass it the results.
        /// </summary>
        void ISpEngineSite.CompleteSkip(int ulNumSkipped)
        {
            _site.CompleteSkip(ulNumSkipped);
        }

        /// <summary>
        /// Load a file either from a local network or from the Internet.
        /// </summary>
        void ISpEngineSite.LoadResource(string uri, ref string? mediaType, out IStream? stream)
        {
            mediaType = null;
#pragma warning disable 56518 // BinaryReader can't be disposed because underlying stream still in use.
            try
            {
                // Get the mime type
                Stream localStream = _site.LoadResource(new Uri(uri, UriKind.RelativeOrAbsolute), mediaType)!;
                BinaryReader reader = new(localStream);
                byte[]? waveFormat = System.Speech.Internal.Synthesis.AudioBase.GetWaveFormat(reader);
                mediaType = null;
                if (waveFormat != null)
                {
                    WAVEFORMATEX hdr = WAVEFORMATEX.ToWaveHeader(waveFormat);
                    switch ((WaveFormatId)hdr.wFormatTag)
                    {
                        case WaveFormatId.Alaw:
                        case WaveFormatId.Mulaw:
                        case WaveFormatId.Pcm:
                            mediaType = "audio/x-wav";
                            break;
                    }
                }
                localStream.Position = 0;
                stream = new SpStreamWrapper(localStream);
            }
            catch
            {
                stream = null;
            }
#pragma warning restore 56518
        }

        #endregion

        #region private Fields

        private EngineSite _site;

        private enum WaveFormatId
        {
            Pcm = 1,
            Alaw = 0x0006,
            Mulaw = 0x0007,
        }

        #endregion
    }

    #region Internal Interfaces
    [ComImport, Guid("9880499B-CCE9-11D2-B503-00C04F797396"), System.Runtime.InteropServices.InterfaceTypeAttribute(1)]
    internal interface ISpEngineSite
    {
        void AddEvents([MarshalAs(UnmanagedType.LPArray, SizeParamIndex = 1)] SpeechEventSapi[] events, int count);
        void GetEventInterest(out long eventInterest);
        [PreserveSig]
        int GetActions();
        void Write(IntPtr data, int count, IntPtr bytesWritten);
        void GetRate(out int rate);
        void GetVolume(out short volume);
        void GetSkipInfo(out int type, out int count);
        void CompleteSkip(int skipped);
        void LoadResource([MarshalAs(UnmanagedType.LPWStr)] string resource, ref string? mediaType, out IStream? stream);
    }

    [StructLayout(LayoutKind.Sequential)]
    internal struct SpeechEventSapi : IEquatable<SpeechEventSapi>
    {
        public short EventId;
        public short ParameterType;
        public int StreamNumber;
        public long AudioStreamOffset;
        public nint Param1;   // Always just a numeric type - contains no unmanaged resources so does not need special clean-up.
        public IntPtr Param2;   // Can be a numeric type, or pointer to string or object. Use SafeSapiLParamHandle to cleanup.

        public static bool operator ==(SpeechEventSapi event1, SpeechEventSapi event2) => event1.Equals(event2);
        public static bool operator !=(SpeechEventSapi event1, SpeechEventSapi event2) => !event1.Equals(event2);

        public override bool Equals([NotNullWhen(true)] object? obj) =>
            obj is SpeechEventSapi other && Equals(other);

        public bool Equals(SpeechEventSapi other) =>
            EventId == other.EventId &&
            ParameterType == other.ParameterType &&
            StreamNumber == other.StreamNumber &&
            AudioStreamOffset == other.AudioStreamOffset &&
            Param1 == other.Param1 &&
            Param2 == other.Param2;

        public override int GetHashCode() => base.GetHashCode();
    }

    #endregion
}