File: Recognition\SpeechRecognitionEngine.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.Collections.ObjectModel;
using System.ComponentModel;
using System.Diagnostics.CodeAnalysis;
using System.Globalization;
using System.IO;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using System.Speech.AudioFormat;
using System.Speech.Internal;
using System.Speech.Internal.ObjectTokens;
using System.Speech.Internal.SapiInterop;

namespace System.Speech.Recognition
{
    public class SpeechRecognitionEngine : IDisposable
    {
        #region Constructors
        public SpeechRecognitionEngine()
        {
            Initialize(null);
        }
        public SpeechRecognitionEngine(CultureInfo culture)
        {
            ArgumentNullException.ThrowIfNull(culture);

            if (culture.Equals(CultureInfo.InvariantCulture))
            {
                throw new ArgumentException(SR.Get(SRID.InvariantCultureInfo), nameof(culture));
            }

            // Enumerate using collection. It would also be possible to directly access the token from SAPI.
            foreach (RecognizerInfo recognizerInfo in InstalledRecognizers())
            {
                if (culture.Equals(recognizerInfo.Culture))
                {
                    Initialize(recognizerInfo);
                    return;
                }
            }
            // No exact match for the culture, try out with a SR engine of the same base culture.
            foreach (RecognizerInfo recognizerInfo in InstalledRecognizers())
            {
                if (Helpers.CompareInvariantCulture(recognizerInfo.Culture, culture))
                {
                    Initialize(recognizerInfo);
                    return;
                }
            }

            // No match even with culture having the same parent
            throw new ArgumentException(SR.Get(SRID.RecognizerNotFound), nameof(culture));
        }
        public SpeechRecognitionEngine(string recognizerId)
        {
            Helpers.ThrowIfEmptyOrNull(recognizerId, nameof(recognizerId));

            foreach (RecognizerInfo recognizerInfo in InstalledRecognizers())
            {
                if (recognizerId.Equals(recognizerInfo.Id, StringComparison.OrdinalIgnoreCase))
                {
                    Initialize(recognizerInfo);
                    return;
                }
            }

            throw new ArgumentException(SR.Get(SRID.RecognizerNotFound), nameof(recognizerId));
        }
        public SpeechRecognitionEngine(RecognizerInfo recognizerInfo)
        {
            ArgumentNullException.ThrowIfNull(recognizerInfo);

            Initialize(recognizerInfo);
        }
        public void Dispose()
        {
            Dispose(true);
            GC.SuppressFinalize(this);
        }
        protected virtual void Dispose(bool disposing)
        {
            if (disposing && !_disposed)
            {
                if (_recognizerBase != null)
                {
                    _recognizerBase.Dispose();
                    _recognizerBase = null;
                }
                if (_sapiRecognizer != null)
                {
                    _sapiRecognizer.Dispose();
                    _sapiRecognizer = null!;
                }
                _disposed = true; // Don't set RecognizerBase to null as every method will then need to throw ObjectDisposedException.
            }
        }

        #endregion

        #region Static Methods

        // Get attributes of all the recognizers that are installed
        public static ReadOnlyCollection<RecognizerInfo> InstalledRecognizers()
        {
            List<RecognizerInfo> recognizers = new();

            // Get list of ObjectTokens
            using (ObjectTokenCategory? category = ObjectTokenCategory.Create(SAPICategories.Recognizers))
            {
                if (category != null)
                {
                    // For each element in list
                    foreach (ObjectToken token in (IEnumerable<ObjectToken>)category)
                    {
                        // Create RecognizerInfo + add to collection
                        RecognizerInfo? recognizerInfo = RecognizerInfo.Create(token);

                        if (recognizerInfo == null)
                        {
                            // But if this entry has a corrupt registry entry then skip it.
                            // Otherwise one bogus entry prevents the whole method from working.
                            continue;
                        }
                        recognizers.Add(recognizerInfo);
                    }
                }
            }
            return new ReadOnlyCollection<RecognizerInfo>(recognizers);
        }

        #endregion

        #region public Properties

        // Settings:
        [EditorBrowsable(EditorBrowsableState.Advanced)]
        public TimeSpan InitialSilenceTimeout
        {
            get { return RecoBase.InitialSilenceTimeout; }
            set { RecoBase.InitialSilenceTimeout = value; }
        }
        [EditorBrowsable(EditorBrowsableState.Advanced)]
        public TimeSpan BabbleTimeout
        {
            get { return RecoBase.BabbleTimeout; }
            set { RecoBase.BabbleTimeout = value; }
        }
        [EditorBrowsable(EditorBrowsableState.Advanced)]
        public TimeSpan EndSilenceTimeout
        {
            get { return TimeSpan.FromMilliseconds(RecoBase.QueryRecognizerSettingAsInt(SapiConstants.SPPROP_RESPONSE_SPEED)); }
            set
            {
                if (value.TotalMilliseconds < 0.0f || value.TotalMilliseconds > 10000.0f)
                {
                    throw new ArgumentOutOfRangeException(nameof(value), SR.Get(SRID.EndSilenceOutOfRange));
                }
                RecoBase.UpdateRecognizerSetting(SapiConstants.SPPROP_RESPONSE_SPEED, (int)value.TotalMilliseconds);
            }
        }
        [EditorBrowsable(EditorBrowsableState.Advanced)]
        public TimeSpan EndSilenceTimeoutAmbiguous
        {
            get { return TimeSpan.FromMilliseconds(RecoBase.QueryRecognizerSettingAsInt(SapiConstants.SPPROP_COMPLEX_RESPONSE_SPEED)); }
            set
            {
                if (value.TotalMilliseconds < 0.0f || value.TotalMilliseconds > 10000.0f)
                {
                    throw new ArgumentOutOfRangeException(nameof(value), SR.Get(SRID.EndSilenceOutOfRange));
                }
                RecoBase.UpdateRecognizerSetting(SapiConstants.SPPROP_COMPLEX_RESPONSE_SPEED, (int)value.TotalMilliseconds);
            }
        }

        // Gives access to the collection of grammars that are currently active. Read-only.
        public ReadOnlyCollection<Grammar> Grammars
        {
            get { return RecoBase.Grammars; }
        }

        // Gives access to the set of attributes exposed by this recognizer.
        public RecognizerInfo RecognizerInfo
        {
            get { return RecoBase.RecognizerInfo; }
        }

        // Data on the audio stream the recognizer is processing
        public AudioState AudioState
        {
            get { return RecoBase.AudioState; }
        }

        // Data on the audio stream the recognizer is processing
        public int AudioLevel
        {
            get { return RecoBase.AudioLevel; }
        }

        // Data on the audio stream the recognizer is processing
        public TimeSpan RecognizerAudioPosition
        {
            get { return RecoBase.RecognizerAudioPosition; }
        }

        // Data on the audio stream the recognizer is processing
        public TimeSpan AudioPosition
        {
            get { return RecoBase.AudioPosition; }
        }
        public SpeechAudioFormatInfo? AudioFormat
        {
            get { return RecoBase.AudioFormat; }
        }
        public int MaxAlternates
        {
            get { return RecoBase.MaxAlternates; }
            set { RecoBase.MaxAlternates = value; }
        }

        #endregion

        #region public Methods
        public void SetInputToWaveFile(string path)
        {
            Helpers.ThrowIfEmptyOrNull(path, nameof(path));

            RecoBase.SetInput(path);
        }
        public void SetInputToWaveStream(Stream audioSource)
        {
            RecoBase.SetInput(audioSource, null);
        }
        public void SetInputToAudioStream(Stream audioSource, SpeechAudioFormatInfo audioFormat)
        {
            ArgumentNullException.ThrowIfNull(audioSource);
            ArgumentNullException.ThrowIfNull(audioFormat);

            RecoBase.SetInput(audioSource, audioFormat);
        }

        // Detach the audio input
        public void SetInputToNull()
        {
            RecoBase.SetInput(null, null);
        }

        // Data on the audio stream the recognizer is processing
        public void SetInputToDefaultAudioDevice()
        {
            RecoBase.SetInputToDefaultAudioDevice();
        }

        // Methods to control recognition process:

        // Does a single synchronous Recognition and then stops the audio stream.
        // Returns null if there was a timeout. Throws on error.
        public RecognitionResult? Recognize()
        {
            return RecoBase.Recognize(RecoBase.InitialSilenceTimeout);
        }
        public RecognitionResult? Recognize(TimeSpan initialSilenceTimeout)
        {
            if (Grammars.Count == 0)
            {
                throw new InvalidOperationException(SR.Get(SRID.RecognizerHasNoGrammar));
            }

            return RecoBase.Recognize(initialSilenceTimeout);
        }

        // Does a single asynchronous Recognition and then stops the audio stream.
        public void RecognizeAsync()
        {
            RecognizeAsync(RecognizeMode.Single);
        }

        // Can do either a single or multiple recognitions depending on the mode.
        public void RecognizeAsync(RecognizeMode mode)
        {
            if (Grammars.Count == 0)
            {
                throw new InvalidOperationException(SR.Get(SRID.RecognizerHasNoGrammar));
            }

            RecoBase.RecognizeAsync(mode);
        }

        // This method stops recognition immediately without completing processing the audio. Then a RecognizeCompelted event is sent.
        public void RecognizeAsyncCancel()
        {
            RecoBase.RecognizeAsyncCancel();
        }

        // This method stops recognition but audio currently buffered is still processed, so a final SpeechRecognized event may be sent {before the RecognizeCompleted event}.
        public void RecognizeAsyncStop()
        {
            RecoBase.RecognizeAsyncStop();
        }

        // Note: Currently this can't be exposed as a true collection in Yakima {it can't be enumerated}. If we think this would be useful we could do this.
        public object QueryRecognizerSetting(string settingName)
        {
            return RecoBase.QueryRecognizerSetting(settingName);
        }
        public void UpdateRecognizerSetting(string settingName, string updatedValue)
        {
            RecoBase.UpdateRecognizerSetting(settingName, updatedValue);
        }
        public void UpdateRecognizerSetting(string settingName, int updatedValue)
        {
            RecoBase.UpdateRecognizerSetting(settingName, updatedValue);
        }
        public void LoadGrammar(Grammar grammar)
        {
            RecoBase.LoadGrammar(grammar);
        }
        public void LoadGrammarAsync(Grammar grammar)
        {
            RecoBase.LoadGrammarAsync(grammar);
        }
        public void UnloadGrammar(Grammar grammar)
        {
            RecoBase.UnloadGrammar(grammar);
        }
        public void UnloadAllGrammars()
        {
            RecoBase.UnloadAllGrammars();
        }
        public RecognitionResult? EmulateRecognize(string inputText)
        {
            return EmulateRecognize(inputText, CompareOptions.IgnoreCase | CompareOptions.IgnoreKanaType | CompareOptions.IgnoreWidth);
        }
        public RecognitionResult? EmulateRecognize(string inputText, CompareOptions compareOptions)
        {
            if (Grammars.Count == 0)
            {
                throw new InvalidOperationException(SR.Get(SRID.RecognizerHasNoGrammar));
            }

            return RecoBase.EmulateRecognize(inputText, compareOptions);
        }
        public RecognitionResult? EmulateRecognize(RecognizedWordUnit[] wordUnits, CompareOptions compareOptions)
        {
            if (Grammars.Count == 0)
            {
                throw new InvalidOperationException(SR.Get(SRID.RecognizerHasNoGrammar));
            }

            return RecoBase.EmulateRecognize(wordUnits, compareOptions);
        }
        public void EmulateRecognizeAsync(string inputText)
        {
            EmulateRecognizeAsync(inputText, CompareOptions.IgnoreCase | CompareOptions.IgnoreKanaType | CompareOptions.IgnoreWidth);
        }
        public void EmulateRecognizeAsync(string inputText, CompareOptions compareOptions)
        {
            if (Grammars.Count == 0)
            {
                throw new InvalidOperationException(SR.Get(SRID.RecognizerHasNoGrammar));
            }

            RecoBase.EmulateRecognizeAsync(inputText, compareOptions);
        }
        public void EmulateRecognizeAsync(RecognizedWordUnit[] wordUnits, CompareOptions compareOptions)
        {
            if (Grammars.Count == 0)
            {
                throw new InvalidOperationException(SR.Get(SRID.RecognizerHasNoGrammar));
            }

            RecoBase.EmulateRecognizeAsync(wordUnits, compareOptions);
        }

        // Methods to pause the recognizer to do atomic updates:
        public void RequestRecognizerUpdate()
        {
            RecoBase.RequestRecognizerUpdate();
        }
        public void RequestRecognizerUpdate(object userToken)
        {
            RecoBase.RequestRecognizerUpdate(userToken);
        }
        public void RequestRecognizerUpdate(object userToken, TimeSpan audioPositionAheadToRaiseUpdate)
        {
            RecoBase.RequestRecognizerUpdate(userToken, audioPositionAheadToRaiseUpdate);
        }

        #endregion

        #region public Events

        // Fired when the RecognizeAsync process completes.
        public event EventHandler<RecognizeCompletedEventArgs>? RecognizeCompleted;

        // Fired when the RecognizeAsync process completes.
        public event EventHandler<EmulateRecognizeCompletedEventArgs>? EmulateRecognizeCompleted;
        public event EventHandler<LoadGrammarCompletedEventArgs>? LoadGrammarCompleted;

        // The event fired when speech is detected. Used for barge-in.
        public event EventHandler<SpeechDetectedEventArgs>? SpeechDetected;

        // The event fired on a recognition.
        public event EventHandler<SpeechRecognizedEventArgs>? SpeechRecognized;

        // The event fired on a no recognition
        public event EventHandler<SpeechRecognitionRejectedEventArgs>? SpeechRecognitionRejected;
        public event EventHandler<RecognizerUpdateReachedEventArgs>? RecognizerUpdateReached;

        // Occurs when a spoken phrase is partially recognized.
        public event EventHandler<SpeechHypothesizedEventArgs>? SpeechHypothesized
        {
            [MethodImplAttribute(MethodImplOptions.Synchronized)]
            add
            {
                ArgumentNullException.ThrowIfNull(value);
                if (_speechHypothesizedDelegate == null)
                {
                    RecoBase.SpeechHypothesized += SpeechHypothesizedProxy;
                }
                _speechHypothesizedDelegate += value;
            }

            [MethodImplAttribute(MethodImplOptions.Synchronized)]
            remove
            {
                ArgumentNullException.ThrowIfNull(value);
                _speechHypothesizedDelegate -= value;
                if (_speechHypothesizedDelegate == null)
                {
                    RecoBase.SpeechHypothesized -= SpeechHypothesizedProxy;
                }
            }
        }
        public event EventHandler<AudioSignalProblemOccurredEventArgs>? AudioSignalProblemOccurred
        {
            [MethodImplAttribute(MethodImplOptions.Synchronized)]
            add
            {
                ArgumentNullException.ThrowIfNull(value);
                if (_audioSignalProblemOccurredDelegate == null)
                {
                    RecoBase.AudioSignalProblemOccurred += AudioSignalProblemOccurredProxy;
                }
                _audioSignalProblemOccurredDelegate += value;
            }

            [MethodImplAttribute(MethodImplOptions.Synchronized)]
            remove
            {
                ArgumentNullException.ThrowIfNull(value);
                _audioSignalProblemOccurredDelegate -= value;
                if (_audioSignalProblemOccurredDelegate == null)
                {
                    RecoBase.AudioSignalProblemOccurred -= AudioSignalProblemOccurredProxy;
                }
            }
        }
        public event EventHandler<AudioLevelUpdatedEventArgs>? AudioLevelUpdated
        {
            [MethodImplAttribute(MethodImplOptions.Synchronized)]
            add
            {
                ArgumentNullException.ThrowIfNull(value);
                if (_audioLevelUpdatedDelegate == null)
                {
                    RecoBase.AudioLevelUpdated += AudioLevelUpdatedProxy;
                }
                _audioLevelUpdatedDelegate += value;
            }

            [MethodImplAttribute(MethodImplOptions.Synchronized)]
            remove
            {
                ArgumentNullException.ThrowIfNull(value);
                _audioLevelUpdatedDelegate -= value;
                if (_audioLevelUpdatedDelegate == null)
                {
                    RecoBase.AudioLevelUpdated -= AudioLevelUpdatedProxy;
                }
            }
        }
        public event EventHandler<AudioStateChangedEventArgs>? AudioStateChanged
        {
            [MethodImplAttribute(MethodImplOptions.Synchronized)]
            add
            {
                ArgumentNullException.ThrowIfNull(value);
                if (_audioStateChangedDelegate == null)
                {
                    RecoBase.AudioStateChanged += AudioStateChangedProxy;
                }
                _audioStateChangedDelegate += value;
            }

            [MethodImplAttribute(MethodImplOptions.Synchronized)]
            remove
            {
                ArgumentNullException.ThrowIfNull(value);
                _audioStateChangedDelegate -= value;
                if (_audioStateChangedDelegate == null)
                {
                    RecoBase.AudioStateChanged -= AudioStateChangedProxy;
                }
            }
        }

        #endregion

        #region Private Methods

        [MemberNotNull(nameof(_sapiRecognizer))]
        private void Initialize(RecognizerInfo? recognizerInfo)
        {
            try
            {
                _sapiRecognizer = new SapiRecognizer(SapiRecognizer.RecognizerType.InProc);
            }
            catch (COMException e)
            {
                throw RecognizerBase.ExceptionFromSapiCreateRecognizerError(e);
            }

            if (recognizerInfo != null)
            {
                ObjectToken token = recognizerInfo.GetObjectToken();
                if (token == null)
                {
                    throw new ArgumentException(SR.Get(SRID.NullParamIllegal), nameof(recognizerInfo));
                }
                try
                {
                    _sapiRecognizer.SetRecognizer(token.SAPIToken);
                }
                catch (COMException e)
                {
                    throw new ArgumentException(SR.Get(SRID.RecognizerNotFound), RecognizerBase.ExceptionFromSapiCreateRecognizerError(e));
                }
            }

            // For the SpeechRecognitionEngine we don't want recognition to start until the Recognize() or RecognizeAsync() methods are called.
            _sapiRecognizer.SetRecoState(SPRECOSTATE.SPRST_INACTIVE);
        }

        // Proxy event handlers used to translate the sender from the RecognizerBase to this class:

        private void RecognizeCompletedProxy(object? sender, RecognizeCompletedEventArgs e)
        {
            RecognizeCompleted?.Invoke(this, e);
        }

        private void EmulateRecognizeCompletedProxy(object? sender, EmulateRecognizeCompletedEventArgs e)
        {
            EmulateRecognizeCompleted?.Invoke(this, e);
        }

        private void LoadGrammarCompletedProxy(object? sender, LoadGrammarCompletedEventArgs e)
        {
            LoadGrammarCompleted?.Invoke(this, e);
        }

        private void SpeechDetectedProxy(object? sender, SpeechDetectedEventArgs e)
        {
            SpeechDetected?.Invoke(this, e);
        }

        private void SpeechRecognizedProxy(object? sender, SpeechRecognizedEventArgs e)
        {
            SpeechRecognized?.Invoke(this, e);
        }

        private void SpeechRecognitionRejectedProxy(object? sender, SpeechRecognitionRejectedEventArgs e)
        {
            SpeechRecognitionRejected?.Invoke(this, e);
        }

        private void RecognizerUpdateReachedProxy(object? sender, RecognizerUpdateReachedEventArgs e)
        {
            RecognizerUpdateReached?.Invoke(this, e);
        }

        private void SpeechHypothesizedProxy(object? sender, SpeechHypothesizedEventArgs e)
        {
            _speechHypothesizedDelegate?.Invoke(this, e);
        }

        private void AudioSignalProblemOccurredProxy(object? sender, AudioSignalProblemOccurredEventArgs e)
        {
            _audioSignalProblemOccurredDelegate?.Invoke(this, e);
        }

        private void AudioLevelUpdatedProxy(object? sender, AudioLevelUpdatedEventArgs e)
        {
            _audioLevelUpdatedDelegate?.Invoke(this, e);
        }

        private void AudioStateChangedProxy(object? sender, AudioStateChangedEventArgs e)
        {
            _audioStateChangedDelegate?.Invoke(this, e);
        }

        #endregion

        #region Private Properties
        private RecognizerBase RecoBase
        {
            get
            {
                if (_disposed)
                {
                    throw new ObjectDisposedException("SpeechRecognitionEngine");
                }
                if (_recognizerBase == null)
                {
                    _recognizerBase = new RecognizerBase(_sapiRecognizer, true);

                    // Add event handlers for low-overhead events:
                    _recognizerBase.RecognizeCompleted += RecognizeCompletedProxy;
                    _recognizerBase.EmulateRecognizeCompleted += EmulateRecognizeCompletedProxy;
                    _recognizerBase.LoadGrammarCompleted += LoadGrammarCompletedProxy;
                    _recognizerBase.SpeechDetected += SpeechDetectedProxy;
                    _recognizerBase.SpeechRecognized += SpeechRecognizedProxy;
                    _recognizerBase.SpeechRecognitionRejected += SpeechRecognitionRejectedProxy;
                    _recognizerBase.RecognizerUpdateReached += RecognizerUpdateReachedProxy;
                }
                return _recognizerBase;
            }
        }
        #endregion

        #region Private Fields

        private bool _disposed;
        private RecognizerBase? _recognizerBase;
        private SapiRecognizer _sapiRecognizer;

        private EventHandler<AudioSignalProblemOccurredEventArgs>? _audioSignalProblemOccurredDelegate;
        private EventHandler<AudioLevelUpdatedEventArgs>? _audioLevelUpdatedDelegate;
        private EventHandler<AudioStateChangedEventArgs>? _audioStateChangedDelegate;

        private EventHandler<SpeechHypothesizedEventArgs>? _speechHypothesizedDelegate;

        #endregion
    }
}