File: Synthesis\SpeechSynthesizer.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.ObjectModel;
using System.Globalization;
using System.IO;
using System.Runtime.CompilerServices;
using System.Speech.AudioFormat;
using System.Speech.Internal;
using System.Speech.Internal.Synthesis;
using System.Speech.Synthesis.TtsEngine;

namespace System.Speech.Synthesis
{
    public sealed class SpeechSynthesizer : IDisposable
    {
        #region Constructors
        public SpeechSynthesizer()
        {
        }

        ~SpeechSynthesizer()
        {
            Dispose(false);
        }
        public void Dispose()
        {
            Dispose(true);
            GC.SuppressFinalize(this);
        }

        #endregion

        #region public Methods
        public void SelectVoice(string name)
        {
            Helpers.ThrowIfEmptyOrNull(name, nameof(name));
            TTSVoice ttsVoice = VoiceSynthesizer.GetEngine(name, CultureInfo.CurrentUICulture, VoiceGender.NotSet, VoiceAge.NotSet, 1, true);

            if (ttsVoice == null || name != ttsVoice.VoiceInfo.Name)
            {
                // No match - throw
                throw new ArgumentException(SR.Get(SRID.SynthesizerSetVoiceNoMatch));
            }
            VoiceSynthesizer.Voice = ttsVoice;
        }
        public void SelectVoiceByHints(VoiceGender gender)
        {
            SelectVoiceByHints(gender, VoiceAge.NotSet, 1, CultureInfo.CurrentUICulture);
        }
        public void SelectVoiceByHints(VoiceGender gender, VoiceAge age)
        {
            SelectVoiceByHints(gender, age, 1, CultureInfo.CurrentUICulture);
        }
        public void SelectVoiceByHints(VoiceGender gender, VoiceAge age, int voiceAlternate)
        {
            SelectVoiceByHints(gender, age, voiceAlternate, CultureInfo.CurrentUICulture);
        }
        public void SelectVoiceByHints(VoiceGender gender, VoiceAge age, int voiceAlternate, CultureInfo culture)
        {
            ArgumentNullException.ThrowIfNull(culture);

            if (voiceAlternate < 0)
            {
                throw new ArgumentOutOfRangeException(nameof(voiceAlternate), SR.Get(SRID.PromptBuilderInvalidVariant));
            }
            if (!VoiceInfo.ValidateGender(gender))
            {
                throw new ArgumentException(SR.Get(SRID.EnumInvalid, "VoiceGender"), nameof(gender));
            }

            if (!VoiceInfo.ValidateAge(age))
            {
                throw new ArgumentException(SR.Get(SRID.EnumInvalid, "VoiceAge"), nameof(age));
            }

            TTSVoice ttsVoice = VoiceSynthesizer.GetEngine(null, culture, gender, age, voiceAlternate, true);

            if (ttsVoice == null)
            {
                // No match - throw
                throw new InvalidOperationException(SR.Get(SRID.SynthesizerSetVoiceNoMatch));
            }
            VoiceSynthesizer.Voice = ttsVoice;
        }
        public Prompt SpeakAsync(string textToSpeak)
        {
            ArgumentNullException.ThrowIfNull(textToSpeak);

            Prompt prompt = new(textToSpeak, SynthesisTextFormat.Text);
            SpeakAsync(prompt);
            return prompt;
        }
        public void SpeakAsync(Prompt prompt)
        {
            ArgumentNullException.ThrowIfNull(prompt);

            prompt.Synthesizer = this;
            VoiceSynthesizer.SpeakAsync(prompt);
        }
        public Prompt SpeakSsmlAsync(string textToSpeak)
        {
            ArgumentNullException.ThrowIfNull(textToSpeak);

            Prompt prompt = new(textToSpeak, SynthesisTextFormat.Ssml);
            SpeakAsync(prompt);
            return prompt;
        }
        public Prompt SpeakAsync(PromptBuilder promptBuilder)
        {
            ArgumentNullException.ThrowIfNull(promptBuilder);

            Prompt prompt = new(promptBuilder);
            SpeakAsync(prompt);
            return prompt;
        }
        public void Speak(string textToSpeak)
        {
            Speak(new Prompt(textToSpeak, SynthesisTextFormat.Text));
        }
        public void Speak(Prompt prompt)
        {
            ArgumentNullException.ThrowIfNull(prompt);

            // Avoid a dead lock if the synthesizer is Paused
            if (State == SynthesizerState.Paused)
            {
                throw new InvalidOperationException(SR.Get(SRID.SynthesizerSyncSpeakWhilePaused));
            }

            prompt.Synthesizer = this;
            prompt._syncSpeak = true;
            VoiceSynthesizer.Speak(prompt);
        }
        public void Speak(PromptBuilder promptBuilder)
        {
            Speak(new Prompt(promptBuilder));
        }
        public void SpeakSsml(string textToSpeak)
        {
            Speak(new Prompt(textToSpeak, SynthesisTextFormat.Ssml));
        }

        /// <summary>
        /// Pause the playback of all speech in this synthesizer.
        /// </summary>
        public void Pause()
        {
            // Increment the Paused count
            if (!_paused)
            {
                VoiceSynthesizer.Pause();
                _paused = true;
            }
        }

        /// <summary>
        /// Resume the playback of all speech in this synthesizer.
        /// </summary>
        public void Resume()
        {
            if (_paused)
            {
                VoiceSynthesizer.Resume();
                _paused = false;
            }
        }

        /// <summary>
        /// Cancel playback of all Prompts currently in the queue.
        /// </summary>
        public void SpeakAsyncCancel(Prompt prompt)
        {
            ArgumentNullException.ThrowIfNull(prompt);

            VoiceSynthesizer.Abort(prompt);
        }

        /// <summary>
        /// Cancel playback of all Prompts currently in the queue.
        /// </summary>
        public void SpeakAsyncCancelAll()
        {
            VoiceSynthesizer.Abort();
        }
        // The stream is disposed when the speech synthesizer is disposed
        public void SetOutputToWaveFile(string path)
        {
            Helpers.ThrowIfEmptyOrNull(path, nameof(path));

            SetOutputToNull();
            SetOutputStream(new FileStream(path, FileMode.Create, FileAccess.Write), null, true, true);
        }
        // The stream is disposed when the speech synthesizer is disposed
        public void SetOutputToWaveFile(string path, SpeechAudioFormatInfo formatInfo)
        {
            Helpers.ThrowIfEmptyOrNull(path, nameof(path));
            ArgumentNullException.ThrowIfNull(formatInfo);

            SetOutputToNull();
            SetOutputStream(new FileStream(path, FileMode.Create, FileAccess.Write), formatInfo, true, true);
        }
        public void SetOutputToWaveStream(Stream audioDestination)
        {
            ArgumentNullException.ThrowIfNull(audioDestination);

            SetOutputStream(audioDestination, null, true, false);
        }
        public void SetOutputToAudioStream(Stream audioDestination, SpeechAudioFormatInfo formatInfo)
        {
            ArgumentNullException.ThrowIfNull(audioDestination);
            ArgumentNullException.ThrowIfNull(formatInfo);

            SetOutputStream(audioDestination, formatInfo, false, false);
        }
        public void SetOutputToDefaultAudioDevice()
        {
            SetOutputStream(null, null, true, false);
        }
        // The stream is disposed when the speech synthesizer is disposed
        public void SetOutputToNull()
        {
            // Close the existing stream
            if (_outputStream != Stream.Null)
            {
                VoiceSynthesizer.SetOutput(Stream.Null, null, true);
            }

            if (_outputStream != null)
            {
                if (_closeStreamOnExit)
                {
                    _outputStream.Close();
                }
            }
            _outputStream = Stream.Null;
        }
        // Dynamic content, use a method instead of a property to denote that fact
        public Prompt? GetCurrentlySpokenPrompt()
        {
            return VoiceSynthesizer.Prompt;
        }
        public ReadOnlyCollection<InstalledVoice> GetInstalledVoices()
        {
            return VoiceSynthesizer.GetInstalledVoices(null);
        }
        public ReadOnlyCollection<InstalledVoice> GetInstalledVoices(CultureInfo culture)
        {
            ArgumentNullException.ThrowIfNull(culture);

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

            return VoiceSynthesizer.GetInstalledVoices(culture);
        }
        public void AddLexicon(Uri uri, string mediaType)
        {
            ArgumentNullException.ThrowIfNull(uri);

            VoiceSynthesizer.AddLexicon(uri, mediaType);
        }
        public void RemoveLexicon(Uri uri)
        {
            ArgumentNullException.ThrowIfNull(uri);

            VoiceSynthesizer.RemoveLexicon(uri);
        }

        #region public Events
        public event EventHandler<SpeakStartedEventArgs>? SpeakStarted
        {
            [MethodImplAttribute(MethodImplOptions.Synchronized)]
            add
            {
                ArgumentNullException.ThrowIfNull(value);
                VoiceSynthesizer._speakStarted += value;
            }
            [MethodImplAttribute(MethodImplOptions.Synchronized)]
            remove
            {
                ArgumentNullException.ThrowIfNull(value);
                VoiceSynthesizer._speakStarted -= value;
            }
        }
        public event EventHandler<SpeakCompletedEventArgs>? SpeakCompleted
        {
            [MethodImplAttribute(MethodImplOptions.Synchronized)]
            add
            {
                ArgumentNullException.ThrowIfNull(value);
                VoiceSynthesizer._speakCompleted += value;
            }
            [MethodImplAttribute(MethodImplOptions.Synchronized)]
            remove
            {
                ArgumentNullException.ThrowIfNull(value);
                VoiceSynthesizer._speakCompleted -= value;
            }
        }
        public event EventHandler<SpeakProgressEventArgs>? SpeakProgress
        {
            [MethodImplAttribute(MethodImplOptions.Synchronized)]
            add
            {
                ArgumentNullException.ThrowIfNull(value);
                VoiceSynthesizer.AddEvent<SpeakProgressEventArgs>(TtsEventId.WordBoundary, ref VoiceSynthesizer._speakProgress, value);
            }
            [MethodImplAttribute(MethodImplOptions.Synchronized)]
            remove
            {
                ArgumentNullException.ThrowIfNull(value);
                VoiceSynthesizer.RemoveEvent<SpeakProgressEventArgs>(TtsEventId.WordBoundary, ref VoiceSynthesizer._speakProgress, value);
            }
        }
        public event EventHandler<BookmarkReachedEventArgs>? BookmarkReached
        {
            [MethodImplAttribute(MethodImplOptions.Synchronized)]
            add
            {
                ArgumentNullException.ThrowIfNull(value);
                VoiceSynthesizer.AddEvent<BookmarkReachedEventArgs>(TtsEventId.Bookmark, ref VoiceSynthesizer._bookmarkReached, value);
            }
            [MethodImplAttribute(MethodImplOptions.Synchronized)]
            remove
            {
                ArgumentNullException.ThrowIfNull(value);
                VoiceSynthesizer.RemoveEvent<BookmarkReachedEventArgs>(TtsEventId.Bookmark, ref VoiceSynthesizer._bookmarkReached, value);
            }
        }
        public event EventHandler<VoiceChangeEventArgs>? VoiceChange
        {
            [MethodImplAttribute(MethodImplOptions.Synchronized)]
            add
            {
                ArgumentNullException.ThrowIfNull(value);
                VoiceSynthesizer.AddEvent<VoiceChangeEventArgs>(TtsEventId.VoiceChange, ref VoiceSynthesizer._voiceChange, value);
            }
            [MethodImplAttribute(MethodImplOptions.Synchronized)]
            remove
            {
                ArgumentNullException.ThrowIfNull(value);
                VoiceSynthesizer.RemoveEvent<VoiceChangeEventArgs>(TtsEventId.VoiceChange, ref VoiceSynthesizer._voiceChange, value);
            }
        }

        #region WinFx
        public event EventHandler<PhonemeReachedEventArgs>? PhonemeReached
        {
            [MethodImplAttribute(MethodImplOptions.Synchronized)]
            add
            {
                ArgumentNullException.ThrowIfNull(value);
                VoiceSynthesizer.AddEvent<PhonemeReachedEventArgs>(TtsEventId.Phoneme, ref VoiceSynthesizer._phonemeReached, value);
            }
            [MethodImplAttribute(MethodImplOptions.Synchronized)]
            remove
            {
                ArgumentNullException.ThrowIfNull(value);
                VoiceSynthesizer.RemoveEvent<PhonemeReachedEventArgs>(TtsEventId.Phoneme, ref VoiceSynthesizer._phonemeReached, value);
            }
        }
        public event EventHandler<VisemeReachedEventArgs>? VisemeReached
        {
            [MethodImplAttribute(MethodImplOptions.Synchronized)]
            add
            {
                ArgumentNullException.ThrowIfNull(value);
                VoiceSynthesizer.AddEvent<VisemeReachedEventArgs>(TtsEventId.Viseme, ref VoiceSynthesizer._visemeReached, value);
            }
            [MethodImplAttribute(MethodImplOptions.Synchronized)]
            remove
            {
                ArgumentNullException.ThrowIfNull(value);
                VoiceSynthesizer.RemoveEvent<VisemeReachedEventArgs>(TtsEventId.Viseme, ref VoiceSynthesizer._visemeReached, value);
            }
        }

        #endregion
        public event EventHandler<StateChangedEventArgs>? StateChanged
        {
            [MethodImplAttribute(MethodImplOptions.Synchronized)]
            add
            {
                ArgumentNullException.ThrowIfNull(value);
                VoiceSynthesizer._stateChanged += value;
            }
            [MethodImplAttribute(MethodImplOptions.Synchronized)]
            remove
            {
                ArgumentNullException.ThrowIfNull(value);
                VoiceSynthesizer._stateChanged -= value;
            }
        }

        #endregion

        #endregion Events

        #region public Properties
        public SynthesizerState State
        {
            get
            {
                return VoiceSynthesizer.State;
            }
        }
        public int Rate
        {
            get
            {
                return VoiceSynthesizer.Rate;
            }
            set
            {
                if (value < -10 || value > 10)
                {
                    throw new ArgumentOutOfRangeException(nameof(value), SR.Get(SRID.RateOutOfRange));
                }
                VoiceSynthesizer.Rate = value;
            }
        }
        public int Volume
        {
            get
            {
                return VoiceSynthesizer.Volume;
            }
            set
            {
                if (value < 0 || value > 100)
                {
                    throw new ArgumentOutOfRangeException(nameof(value), SR.Get(SRID.ResourceUsageOutOfRange));
                }
                VoiceSynthesizer.Volume = value;
            }
        }
        public VoiceInfo Voice
        {
            get
            {
                // Get the sapi voice
                return VoiceSynthesizer.CurrentVoice(true).VoiceInfo;
            }
        }

        #region Internal Properties

        #endregion

        #endregion

        #region Private Methods
        private void SetOutputStream(Stream? stream, SpeechAudioFormatInfo? formatInfo, bool headerInfo, bool closeStreamOnExit)
        {
            SetOutputToNull();
            _outputStream = stream;
            _closeStreamOnExit = closeStreamOnExit;

            // Need to serialize into a proper wav file before closing the stream
            VoiceSynthesizer.SetOutput(stream, formatInfo, headerInfo);
        }
        private void Dispose(bool disposing)
        {
            if (!_isDisposed && disposing)
            {
                if (_voiceSynthesis != null)
                {
                    // flag it first so asynchronous operation has more time to finish
                    _isDisposed = true;
                    SpeakAsyncCancelAll();
                    // Flush the Output stream
                    if (_outputStream != null)
                    {
                        if (_closeStreamOnExit)
                        {
                            _outputStream.Close();
                        }
                        else
                        {
                            _outputStream.Flush();
                        }
                        _outputStream = null;
                    }
                }
            }

            if (_voiceSynthesis != null)
            {
                // Terminate the background synthesis object the thread.
                _voiceSynthesis.Dispose();
                _voiceSynthesis = null;
            }

            _isDisposed = true;
        }

        #endregion

        #region Private Properties
        private VoiceSynthesis VoiceSynthesizer
        {
            get
            {
                if (_voiceSynthesis == null && _isDisposed)
                {
                    throw new ObjectDisposedException("SpeechSynthesizer");
                }
                if (_voiceSynthesis == null)
                {
                    WeakReference wr = new(this);
                    _voiceSynthesis = new VoiceSynthesis(wr);
                }
                return _voiceSynthesis;
            }
        }
        #endregion

        #region Private Fields

        // SpVoice for this synthesizer
        private VoiceSynthesis? _voiceSynthesis;

        // Is the object disposed?
        private bool _isDisposed;

        // Count of number of consecutive calls to Paused
        private bool _paused;

        // .NET Stream - keep a reference to it to avoid it to be GC
        private Stream? _outputStream;

        // If stream were created in SpeechFx then close it, otherwise it should remain open.
        private bool _closeStreamOnExit;

        #endregion Fields
    }

    #region Public Enums
    public enum SynthesizerState
    {
        Ready,
        Speaking,
        Paused
    }
    [Flags]
    public enum SynthesizerEmphasis
    {
        Stressed = 1,
        Emphasized = 2
    }

    #endregion
}