//------------------------------------------------------------------
//
// Copyright (c) Microsoft Corporation. All rights reserved.
//
//
// Description:
// This module is the main implementation file for the CSpVoice class and
// it's associated event management logic. This is the main SAPI5 COM object
// for all of TTS.
//
// History:
// 2/1/2005 jeanfp Created from the Sapi Managed code
//-----------------------------------------------------------------
using System;
using System.IO;
using System.Collections;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.ComponentModel;
using System.Diagnostics;
using System.Globalization;
using System.Reflection;
using System.Runtime.InteropServices;
using System.Security;
using System.Speech.AudioFormat;
using System.Speech.Internal;
using System.Speech.Internal.ObjectTokens;
using System.Speech.Synthesis;
using System.Speech.Synthesis.TtsEngine;
using System.Text;
using System.Threading;
#if (SPEECHSERVER || PROMPT_ENGINE) && !SERVERTESTDLL
using System.Security.Cryptography;
#endif
#pragma warning disable 1634, 1691 // Allows suppression of certain PreSharp messages.
#pragma warning disable 56502 // Empty catch statements
namespace System.Speech.Internal.Synthesis
{
internal sealed class VoiceSynthesis : IDisposable
{
//*******************************************************************
//
// Constructors
//
//*******************************************************************
#region Constructors
internal VoiceSynthesis (WeakReference speechSynthesizer)
{
_asyncWorker = new AsyncSerializedWorker (new WaitCallback (ProcessPostData), null);
_asyncWorkerUI = new AsyncSerializedWorker (null, AsyncOperationManager.CreateOperation (null));
// Setup the event dispatcher for state changed events
_eventStateChanged = new WaitCallback (OnStateChanged);
// Setup the event dispatcher for all other events
_signalWorkerCallback = new WaitCallback (SignalWorkerThread);
//
_speechSyntesizer = speechSynthesizer;
// Initialize the engine site;
_resourceLoader = new ResourceLoader ();
_site = new EngineSite (_resourceLoader);
// No pending work and speaking is done
_evtPendingSpeak.Reset ();
// Create the default audio device (speaker)
#if SPEECHSERVER && !SERVERTESTDLL
_waveOut = new AudioFileOut (Stream.Null, new SpeechAudioFormatInfo (EncodingFormat.ALaw, 8000, 8, 1, 8000, 1, null), false, _asyncWorker);
#else
_waveOut = new AudioDeviceOut (SAPICategories.DefaultDeviceOut (), _asyncWorker);
#endif
// Build the installed voice collection on first run
if (_allVoices == null)
{
_allVoices = BuildInstalledVoices (this);
// If no voice are installed, then bail out.
if (_allVoices.Count == 0)
{
_allVoices = null;
throw new PlatformNotSupportedException (SR.Get (SRID.SynthesizerVoiceFailed));
}
}
// Create a dynamic list of installed voices from the list of all available voices.
_installedVoices = new List (_allVoices.Count);
foreach (InstalledVoice installedVoice in _allVoices)
{
_installedVoices.Add (new InstalledVoice (this, installedVoice.VoiceInfo));
}
// Get the default rate
_site.VoiceRate = _defaultRate = (int) GetDefaultRate ();
// Start the worker thread
_workerThread = new Thread (new ThreadStart (ThreadProc));
_workerThread.IsBackground = true;
_workerThread.Start ();
// Default TTS engines events to be notified
SetInterest (_ttsEvents);
}
~VoiceSynthesis ()
{
Dispose (false);
}
///
///
///
public void Dispose ()
{
Dispose (true);
GC.SuppressFinalize (this);
}
#endregion
//********************************************************************
//
// Internal Methods
//
//*******************************************************************
#region Internal Methods
#region SpeechSynthesis 'public' API implementation
///
/// TODOC
///
///
///
internal void Speak (Prompt prompt)
{
bool done = false;
EventHandler eventHandler = delegate (object sender, StateChangedEventArgs args)
{
if (prompt.IsCompleted && args.State == SynthesizerState.Ready)
{
done = true;
_workerWaitHandle.Set ();
}
};
try
{
_stateChanged += eventHandler;
_asyncWorkerUI.AsyncMode = false;
_asyncWorkerUI.WorkItemPending += _signalWorkerCallback;
// SpeakAsync the prompt
QueuePrompt (prompt);
while (!done && !_isDisposed)
{
_workerWaitHandle.WaitOne ();
_asyncWorkerUI.ConsumeQueue ();
}
// Throw if an exception occured
if (prompt._exception != null)
{
throw prompt._exception;
}
}
finally
{
_asyncWorkerUI.AsyncMode = true;
_asyncWorkerUI.WorkItemPending -= _signalWorkerCallback;
_stateChanged -= eventHandler;
}
}
///
/// TODOC
///
///
///
internal void SpeakAsync (Prompt prompt)
{
QueuePrompt (prompt);
}
#region Speech Synthesis events
internal void OnSpeakStarted (SpeakStartedEventArgs e)
{
if (_speakStarted != null)
{
_asyncWorkerUI.PostOperation (_speakStarted, _speechSyntesizer.Target, e);
}
}
internal void FireSpeakCompleted (object sender, SpeakCompletedEventArgs e)
{
if (_speakCompleted != null && !e.Prompt._syncSpeak)
{
_speakCompleted (sender, e);
}
e.Prompt.Synthesizer = null;
}
internal void OnSpeakCompleted (SpeakCompletedEventArgs e)
{
e.Prompt.IsCompleted = true;
_asyncWorkerUI.PostOperation (new EventHandler (FireSpeakCompleted), _speechSyntesizer.Target, e);
}
internal void OnSpeakProgress (SpeakProgressEventArgs e)
{
if (_speakProgress != null)
{
string text = string.Empty;
if (e.Prompt._media == SynthesisMediaType.Ssml)
{
int length = e.CharacterCount;
text = RemoveEscapeString (e.Prompt._text, e.CharacterPosition, length, out length);
e.CharacterCount = length;
}
else
{
text = e.Prompt._text.Substring (e.CharacterPosition, e.CharacterCount);
}
e.Text = text;
_asyncWorkerUI.PostOperation (_speakProgress, _speechSyntesizer.Target, e);
}
}
private string RemoveEscapeString (string text, int start, int length, out int newLength)
{
newLength = length;
// Find the pos '>' from the start position and so sustitution from this point on
int startInXml = text.LastIndexOf ('>', start);
System.Diagnostics.Debug.Assert (startInXml >= 0);
// Check for special character strings "%gt;", etc... and convert them to "<" etc...
int curPos = startInXml;
StringBuilder sb = new StringBuilder (text.Substring (0, curPos));
do
{
// Look for one of the Xml escape string
int iEscapeString = -1;
int pos = int.MaxValue;
for (int i = 0; i < xmlEscapeStrings.Length; i++)
{
int idx;
if ((idx = text.IndexOf (xmlEscapeStrings [i], curPos, StringComparison.Ordinal)) >= 0)
{
if (pos > idx)
{
pos = idx;
iEscapeString = i;
}
}
}
if (iEscapeString < 0)
{
// If no special string have been found then the current position is the end of the string.
pos = text.Length;
}
else if (pos >= startInXml)
{
// For the character that is replacing the escape sequence.
newLength += xmlEscapeStrings [iEscapeString].Length - 1;
}
else
{
// Found an escape sequence but it is it before the current text fragment.
pos += xmlEscapeStrings [iEscapeString].Length;
iEscapeString = -1;
}
// add the new string
int len = pos - curPos;
sb.Append (text.Substring (curPos, len));
if (iEscapeString >= 0)
{
sb.Append (xmlEscapeChars [iEscapeString]);
int lenEscape = xmlEscapeStrings [iEscapeString].Length;
pos += lenEscape;
}
curPos = pos;
}
while (start + length > sb.Length);
return sb.ToString ().Substring (start, length);
}
internal void OnBookmarkReached (BookmarkReachedEventArgs e)
{
if (_bookmarkReached != null)
{
_asyncWorkerUI.PostOperation (_bookmarkReached, _speechSyntesizer.Target, e);
}
}
internal void OnVoiceChange (VoiceChangeEventArgs e)
{
if (_voiceChange != null)
{
_asyncWorkerUI.PostOperation (_voiceChange, _speechSyntesizer.Target, e);
}
}
#if !SPEECHSERVER
internal void OnPhonemeReached (PhonemeReachedEventArgs e)
{
if (_phonemeReached != null)
{
_asyncWorkerUI.PostOperation (_phonemeReached, _speechSyntesizer.Target, e);
}
}
private void OnVisemeReached (VisemeReachedEventArgs e)
{
if (_visemeReached != null)
{
_asyncWorkerUI.PostOperation (_visemeReached, _speechSyntesizer.Target, e);
}
}
#else
internal void OnProprietaryEngineEvent (ProprietaryEngineEventArgs e)
{
if (_proprietaryEngineEvent != null)
{
_asyncWorkerUI.PostOperation(_proprietaryEngineEvent, _speechSyntesizer.Target, e);
}
}
#endif
private void OnStateChanged (object o)
{
// For all other events the lock is done in the dispatch method
lock (_thisObjectLock)
{
StateChangedEventArgs e = (StateChangedEventArgs) o;
if (_stateChanged != null)
{
_asyncWorkerUI.PostOperation (_stateChanged, _speechSyntesizer.Target, e);
}
}
}
internal void AddEvent (TtsEventId ttsEvent, ref EventHandler internalEventHandler, EventHandler eventHandler) where T : PromptEventArgs
{
lock (_thisObjectLock)
{
Helpers.ThrowIfNull (eventHandler, "eventHandler");
// could through if unsuccessful - delay the SetEventInterest
bool fSetSapiInterest = internalEventHandler == null;
internalEventHandler += eventHandler;
if (fSetSapiInterest)
{
_ttsEvents |= (1 << (int) ttsEvent);
SetInterest (_ttsEvents);
}
}
}
internal void RemoveEvent (TtsEventId ttsEvent, ref EventHandler internalEventHandler, EventHandler eventHandler) where T : EventArgs
{
lock (_thisObjectLock)
{
Helpers.ThrowIfNull (eventHandler, "eventHandler");
// could through if unsuccessful - delay the SetEventInterest
internalEventHandler -= eventHandler;
if (internalEventHandler == null)
{
_ttsEvents &= ~(1 << (int) ttsEvent);
SetInterest (_ttsEvents);
}
}
}
#endregion
#endregion
///
/// TODOC
///
///
///
///
internal void SetOutput (Stream stream, SpeechAudioFormatInfo formatInfo, bool headerInfo)
{
lock (_pendingSpeakQueue)
{
// Output is not supposed to change while speaking.
if (State == SynthesizerState.Speaking)
{
throw new InvalidOperationException (SR.Get (SRID.SynthesizerSetOutputSpeaking));
}
if (State == SynthesizerState.Paused)
{
throw new InvalidOperationException (SR.Get (SRID.SynthesizerSyncSetOutputWhilePaused));
}
lock (_processingSpeakLock)
{
if (stream == null)
{
#if SPEECHSERVER && !SERVERTESTDLL
_waveOut = new AudioFileOut (Stream.Null, null, false, _asyncWorker);
#else
_waveOut = new AudioDeviceOut (SAPICategories.DefaultDeviceOut (), _asyncWorker);
#endif
}
else
{
_waveOut = new AudioFileOut (stream, formatInfo, headerInfo, _asyncWorker);
}
}
}
}
///
/// Description:
/// This method synchronously purges all data that is currently in the
/// rendering pipeline.
///
internal void Abort ()
{
//--- Purge all pending speak requests and reset the voice
lock (_pendingSpeakQueue)
{
lock (_site)
{
if (_currentPrompt != null)
{
_site.Abort ();
_waveOut.Abort ();
}
}
lock (_processingSpeakLock)
{
Parameters [] parameters = _pendingSpeakQueue.ToArray ();
foreach (Parameters parameter in parameters)
{
ParametersSpeak paramSpeak = parameter._parameter as ParametersSpeak;
if (paramSpeak != null)
{
paramSpeak._prompt._exception = new OperationCanceledException (SR.Get (SRID.PromptAsyncOperationCancelled));
}
}
// Restart the worker thread
_evtPendingSpeak.Set ();
}
}
}
///
/// Description:
/// This method synchronously purges all data that is currently in the
/// rendering pipeline.
///
internal void Abort (Prompt prompt)
{
//--- Purge all pending speak requests and reset the voice
lock (_pendingSpeakQueue)
{
bool found = false;
foreach (Parameters parameters in _pendingSpeakQueue)
{
ParametersSpeak paramSpeak = parameters._parameter as ParametersSpeak;
if (paramSpeak._prompt == prompt)
{
paramSpeak._prompt._exception = new OperationCanceledException (SR.Get (SRID.PromptAsyncOperationCancelled));
found = true;
break;
}
}
if (!found)
{
// Not in the list, it could be the current prompt
lock (_site)
{
if (_currentPrompt == prompt)
{
_site.Abort ();
_waveOut.Abort ();
}
}
// Wait for completion
lock (_processingSpeakLock)
{
}
}
}
}
///
/// Pause the audio
///
internal void Pause ()
{
lock (_waveOut)
{
if (_waveOut != null)
{
_waveOut.Pause ();
}
lock (_pendingSpeakQueue)
{
// The pause arrived after a speak call was initiated but before it started to speak
// Simulated a Re
if (_pendingSpeakQueue.Count > 0 && State == SynthesizerState.Ready)
{
OnStateChanged (SynthesizerState.Speaking);
}
OnStateChanged (SynthesizerState.Paused);
}
}
}
///
/// Resume the audio
///
internal void Resume ()
{
lock (_waveOut)
{
if (_waveOut != null)
{
_waveOut.Resume ();
}
lock (_pendingSpeakQueue)
{
if (_pendingSpeakQueue.Count > 0 || _currentPrompt != null)
{
OnStateChanged (SynthesizerState.Speaking);
}
else
{
// The state could be set to paused if the Paused happened after the speak happened
if (State == SynthesizerState.Paused)
{
OnStateChanged (SynthesizerState.Speaking);
}
OnStateChanged (SynthesizerState.Ready);
}
}
}
}
internal void AddLexicon (Uri uri, string mediaType)
{
LexiconEntry lexiconEntry = new LexiconEntry (uri, mediaType);
lock (_processingSpeakLock)
{
foreach (LexiconEntry lexicon in _lexicons)
{
if (lexicon._uri.Equals (uri))
{
throw new InvalidOperationException (SR.Get (SRID.DuplicatedEntry));
}
}
_lexicons.Add (lexiconEntry);
}
}
internal void RemoveLexicon (Uri uri)
{
lock (_processingSpeakLock)
{
foreach (LexiconEntry lexicon in _lexicons)
{
if (lexicon._uri.Equals (uri))
{
_lexicons.Remove (lexicon);
// Bail out found
return;
}
}
throw new InvalidOperationException (SR.Get (SRID.FileNotFound, uri.ToString ()));
}
}
///
/// This method is used to create the Engine voice and initialize the culture
///
///
///
///
///
///
///
///
internal TTSVoice GetEngine (string name, CultureInfo culture, VoiceGender gender, VoiceAge age, int variant, bool switchContext)
{
TTSVoice defaultVoice = _currentVoice != null ? _currentVoice : GetVoice (switchContext);
return GetEngineWithVoice (defaultVoice, null, name, culture, gender, age, variant, switchContext);
}
///
/// Returns the voices for a given (or all cultures)
///
/// Culture or null for all culture
///
internal ReadOnlyCollection GetInstalledVoices (CultureInfo culture)
{
if (culture == null || culture == CultureInfo.InvariantCulture)
{
return new ReadOnlyCollection (_installedVoices);
}
else
{
Collection voices = new Collection ();
// loop all the available voices in the registry
// no check if the voice are valid
foreach (InstalledVoice voice in _installedVoices)
{
// Either all voices if culture is
if (culture.Equals (voice.VoiceInfo.Culture))
{
voices.Add (voice);
}
}
return new ReadOnlyCollection (voices);
}
}
#if SPEECHSERVER || PROMPT_ENGINE
internal void LoadDatabase (string localName, string alias)
{
Helpers.ThrowIfEmptyOrNull (localName, "localName");
ExecuteOnBackgroundThread (Action.LoadDatabase, new ParametersDatabase (localName, alias));
if (_pendingException != null)
{
throw _pendingException;
}
}
internal void UnloadDatabase (string alias)
{
Helpers.ThrowIfEmptyOrNull (alias, "alias");
ExecuteOnBackgroundThread (Action.UnloadDatabase, alias);
if (_pendingException != null)
{
throw _pendingException;
}
}
internal void SetResourceLoader (ISpeechResourceLoader resourceLoader)
{
lock (_processingSpeakLock)
{
_resourceLoader.SetResourceLoader (resourceLoader);
}
}
#endif
#endregion
//********************************************************************
//
// Internal Propperties
//
//********************************************************************
#region Internal Propperties
///
/// TODOC
///
///
internal Prompt Prompt
{
get
{
lock (_pendingSpeakQueue)
{
return _currentPrompt;
}
}
}
///
/// TODOC
///
///
internal SynthesizerState State
{
get
{
return _synthesizerState;
}
}
///
/// TODOC
///
///
internal int Rate
{
set
{
_site.VoiceRate = _defaultRate = value;
}
get
{
return _site.VoiceRate;
}
}
///
/// TODOC
///
///
internal int Volume
{
set
{
_site.VoiceVolume = value;
}
get
{
return _site.VoiceVolume;
}
}
///
/// Set/Get the default voice
///
internal TTSVoice Voice
{
set
{
lock (_defaultVoiceLock)
{
if (_currentVoice == _defaultVoice && value == null)
{
_defaultVoiceInitialized = false;
}
_currentVoice = value;
}
}
}
///
/// Set/Get the default voice
///
internal TTSVoice CurrentVoice (bool switchContext)
{
lock (_defaultVoiceLock)
{
// If no voice defined then get the default voice
if (_currentVoice == null)
{
GetVoice (switchContext);
}
return _currentVoice;
}
}
#if (SPEECHSERVER || PROMPT_ENGINE) && !SERVERTESTDLL
///
/// Equivelant to the C runtime function time_t time(time_t*);
///
private static TimeSpan Time
{
get
{
return (DateTime.UtcNow - time_t_0);
}
}
#endif
#endregion
//*******************************************************************
//
// Internal Fields
//
//********************************************************************
#region Internal Fields
// Internal event handlers
internal EventHandler _stateChanged;
// Internal event handlers
internal EventHandler _speakStarted;
internal EventHandler _speakCompleted;
internal EventHandler _speakProgress;
internal EventHandler _bookmarkReached;
internal EventHandler _voiceChange;
#if !SPEECHSERVER
internal EventHandler _phonemeReached;
internal EventHandler _visemeReached;
#else
internal EventHandler _proprietaryEngineEvent;
#endif
#if SPEECHSERVER || PROMPT_ENGINE
// Interal Flag used by MSS to output text rather than Audio
internal bool _outputAsText;
#endif
#endregion
//*******************************************************************
//
// Private Members
//
//*******************************************************************
#region Private Members
//
//=== ISpThreadTask ===============================================================
//
// These methods implement the ISpThreadTask interface. They will all be called on
// a worker thread.
///
/// This method is the task proc used for text rendering and for event
/// forwarding. It may be called on a worker thread for asynchronous speaking, or
/// it may be called on the client thread for synchronous speaking. If it is
/// called on the client thread, the hExitThreadEvent handle will be null.
///
void ThreadProc ()
{
while (true)
{
Parameters parameters;
_evtPendingSpeak.WaitOne ();
//--- Get the next speak item
lock (_pendingSpeakQueue)
{
if (_pendingSpeakQueue.Count > 0)
{
parameters = _pendingSpeakQueue.Dequeue ();
ParametersSpeak paramSpeak = parameters._parameter as ParametersSpeak;
if (paramSpeak != null)
{
lock (_site)
{
if (_currentPrompt == null && State != SynthesizerState.Paused)
{
OnStateChanged (SynthesizerState.Speaking);
}
_currentPrompt = paramSpeak._prompt;
_waveOut.IsAborted = false;
}
}
else
{
_currentPrompt = null;
}
}
else
{
parameters = null;
}
}
// The client thread may have cleared the list to abort the audio
if (parameters != null)
{
switch (parameters._action)
{
case Action.GetVoice:
{
try
{
_pendingVoice = null;
_pendingException = null;
_pendingVoice = GetProxyEngine ((VoiceInfo) parameters._parameter);
}
#pragma warning disable 6500
catch (Exception e)
{
// this thread cannot be terminated.
_pendingException = e;
}
#pragma warning restore 6500
finally
{
// unlock the client
_evtPendingGetProxy.Set ();
}
}
break;
case Action.SpeakText:
{
ParametersSpeak paramSpeak = (ParametersSpeak) parameters._parameter;
try
{
InjectEvent (TtsEventId.StartInputStream, paramSpeak._prompt, paramSpeak._prompt._exception, null);
if (paramSpeak._prompt._exception == null)
{
// No lexicon yet
List lexicons = new List (); ;
//--- Create a single speak info structure for all the text
TTSVoice voice = _currentVoice != null ? _currentVoice : GetVoice (false);
//--- Create the speak info
SpeakInfo speakInfo = new SpeakInfo (this, voice);
if (paramSpeak._textToSpeak != null)
{
//--- Make sure we have a voice defined by now
if (!paramSpeak._isXml)
{
FragmentState fragmentState = new FragmentState ();
fragmentState.Action = TtsEngineAction.Speak;
fragmentState.Prosody = new Prosody ();
TextFragment textFragment = new TextFragment (fragmentState, string.Copy (paramSpeak._textToSpeak));
speakInfo.AddText (voice, textFragment);
}
else
{
TextFragmentEngine engine = new TextFragmentEngine (speakInfo, paramSpeak._textToSpeak, _pexml, _resourceLoader, lexicons);
SsmlParser.Parse (paramSpeak._textToSpeak, engine, speakInfo.Voice);
}
}
else
{
speakInfo.AddAudio (new AudioData (paramSpeak._audioFile, _resourceLoader));
}
// Add the global synthesizer lexicon
lexicons.AddRange (_lexicons);
System.Diagnostics.Debug.Assert (speakInfo != null);
SpeakText (speakInfo, paramSpeak._prompt, lexicons);
}
ChangeStateToReady (paramSpeak._prompt, paramSpeak._prompt._exception);
}
#pragma warning disable 6500
catch (Exception e)
{
//--- Always inject the end of stream and complete even on failure
// Note: we're not getting the return codes from these so we
// don't overwrite a possible error from above. Also we
// really don't care about these errors.
ChangeStateToReady (paramSpeak._prompt, e);
}
}
break;
#pragma warning restore 6500
#if SPEECHSERVER || PROMPT_ENGINE
case Action.LoadDatabase:
try
{
_pendingException = null;
ParametersDatabase paramLoad = (ParametersDatabase) parameters._parameter;
PromptEngine.LoadDatabase (paramLoad._localName, paramLoad._alias);
}
#pragma warning disable 6500
catch (Exception e)
{
// this thread cannot be terminated.
_pendingException = e;
}
#pragma warning restore 6500
finally
{
// unlock the client
_evtPendingGetProxy.Set ();
}
break;
case Action.UnloadDatabase:
try
{
_pendingException = null;
PromptEngine.UnloadDatabase ((string) parameters._parameter);
}
#pragma warning disable 6500
catch (Exception e)
{
// this thread cannot be terminated.
_pendingException = e;
}
#pragma warning restore 6500
finally
{
// unlock the client
_evtPendingGetProxy.Set ();
}
break;
#endif
default:
System.Diagnostics.Debug.Assert (false, "Unknown Action!");
break;
}
}
//--- Get the next speak item
lock (_pendingSpeakQueue)
{
// if nothing left then reset the wait handle.
if (_pendingSpeakQueue.Count == 0)
{
_evtPendingSpeak.Reset ();
}
}
// check if we need to terminate this thread
if (_fExitWorkerThread)
{
_synthesizerState = SynthesizerState.Ready;
break;
}
}
}
private void AddSpeakParameters (Parameters param)
{
lock (_pendingSpeakQueue)
{
_pendingSpeakQueue.Enqueue (param);
// Start the worker thread if the list was empty
if (_pendingSpeakQueue.Count == 1)
{
_evtPendingSpeak.Set ();
}
}
}
///
/// This method renders the current speak info structure. It may be
/// made up of one or more speech segements, each intended for a different
/// voice/engine.
///
private void SpeakText (SpeakInfo speakInfo, Prompt prompt, List lexicons)
{
VoiceInfo currrentVoiceId = null;
//=== Main processing loop ===========================================
for (SpeechSeg speechSeg; (speechSeg = speakInfo.RemoveFirst ()) != null; )
{
TTSVoice voice;
//--- Update the current rendering engine
voice = speechSeg.Voice;
// Fire the voice change object token if necessary
if (voice != null && (currrentVoiceId == null || !currrentVoiceId.Equals (voice.VoiceInfo)))
{
currrentVoiceId = voice.VoiceInfo;
InjectEvent (TtsEventId.VoiceChange, prompt, null, currrentVoiceId);
}
lock (_processingSpeakLock)
{
if (speechSeg.IsText)
{
//--- Speak the segment
lock (_site)
{
if (_waveOut.IsAborted)
{
_waveOut.IsAborted = false;
//--- Always inject the end of stream and complete event on failure
throw new OperationCanceledException (SR.Get (SRID.PromptAsyncOperationCancelled));
}
_site.InitRun (_waveOut, _defaultRate, prompt);
_waveOut.Begin (voice.WaveFormat (_waveOut.WaveFormat));
}
// Set the Lexicons if any
try
{
// Update the lexicon and set the default events to trap
voice.UpdateLexicons (lexicons);
_site.SetEventsInterest (_ttsInterest);
// Calls GetOutputFormat if needed on the TTS engine
byte [] outputWaveFormat = voice.WaveFormat (_waveOut.WaveFormat);
// Get the TTS engine or a backup voice
ITtsEngineProxy engineProxy = voice.TtsEngine;
#if !SPEECHSERVER
// Set the events specific to the desktop
if ((_ttsInterest & (1 << (int) TtsEventId.Phoneme)) != 0 && engineProxy.EngineAlphabet != AlphabetType.Ipa)
{
_site.EventMapper = new PhonemeEventMapper (_site, PhonemeEventMapper.PhonemeConversion.SapiToIpa, engineProxy.AlphabetConverter);
}
else
{
_site.EventMapper = null;
}
#else
if (_outputAsText)
{
engineProxy = new TextEngine (_site, 0);
}
if (speechSeg.ContainsPrompEngineFragment)
{
// Set the backup voice if the text to speak contains some prompt elements
ITtsEngineProxy voiceEngine = engineProxy;
engineProxy = PromptEngineProxy;
voiceEngine.BackupVoice (_promptEngine);
if (_outputAsText)
{
// If text is requested, set the default TTS engine as outputing raw text.
PromptEngineProxy.GetOutputFormat (IntPtr.Zero);
}
else
{
// Set the desired audio format
SetPromptEngineOutputFormat ();
}
}
#endif
// Call the TTS engine to perform the speak through the proxy layer that
// converts SSML fragments to whatever the TTS engine supports
_site.LastException = null;
engineProxy.Speak (speechSeg.FragmentList, outputWaveFormat);
}
finally
{
_waveOut.WaitUntilDone ();
_waveOut.End ();
}
}
else
{
System.Diagnostics.Debug.Assert (speechSeg.Audio != null);
_waveOut.PlayWaveFile (speechSeg.Audio);
// Done with the audio, release the underlying stream
speechSeg.Audio.Dispose ();
}
lock (_site)
{
// The current prompt has now been played
_currentPrompt = null;
// Check for abort or errors during the play
if (_waveOut.IsAborted || _site.LastException != null)
{
_waveOut.IsAborted = false;
if (_site.LastException != null)
{
Exception lastException = _site.LastException;
_site.LastException = null;
throw lastException;
}
//--- Always inject the end of stream and complete event on failure
throw new OperationCanceledException (SR.Get (SRID.PromptAsyncOperationCancelled));
}
}
}
}
} /* SpeakText */
///
/// Get the user's default rate from the registry
///
private static UInt32 GetDefaultRate ()
{
//--- Read the current user's default rate
UInt32 lCurrRateAd = 0;
using (ObjectTokenCategory category = ObjectTokenCategory.Create (SAPICategories.CurrentUserVoices))
{
if (category != null)
{
category.TryGetDWORD (defaultVoiceRate, ref lCurrRateAd);
}
}
return lCurrRateAd;
}
//
private void InjectEvent (TtsEventId evtId, Prompt prompt, Exception exception, VoiceInfo voiceInfo)
{
// If the prompt is terminated, release it ASAP
if (evtId == TtsEventId.EndInputStream)
{
if (_site.EventMapper != null)
{
_site.EventMapper.FlushEvent ();
}
prompt._exception = exception;
}
int evtMask = 1 << (int) evtId;
if ((evtMask & _ttsInterest) != 0)
{
TTSEvent ttsEvent = new TTSEvent (evtId, prompt, exception, voiceInfo);
_asyncWorker.Post (ttsEvent);
}
}
///
/// Calls the client notification delegate.
///
///
private void OnStateChanged (SynthesizerState state)
{
if (_synthesizerState != state)
{
// Keep the last state
SynthesizerState previousState = _synthesizerState;
_synthesizerState = state;
// Fire the events
if (_eventStateChanged != null)
{
_asyncWorker.PostOperation (_eventStateChanged, new StateChangedEventArgs (state, previousState));
}
}
}
///
/// Set the state to ready if nothing anymore needs to be spoken.
///
private void ChangeStateToReady (Prompt prompt, Exception exception)
{
lock (_waveOut)
{
//--- Get the next speak item
lock (_pendingSpeakQueue)
{
// if nothing left then reset the wait handle.
if (_pendingSpeakQueue.Count == 0)
{
_currentPrompt = null;
System.Diagnostics.Debug.Assert (State == SynthesizerState.Speaking || State == SynthesizerState.Paused);
if (State != SynthesizerState.Paused)
{
// Keep the last state
SynthesizerState previousState = _synthesizerState;
_synthesizerState = SynthesizerState.Ready;
// Fire the notification for end of prompt
InjectEvent (TtsEventId.EndInputStream, prompt, exception, null);
if (_eventStateChanged != null)
{
_asyncWorker.PostOperation (_eventStateChanged, new StateChangedEventArgs (_synthesizerState, previousState));
}
}
else
{
// Pause mode. Send a single notification for end of prompt
InjectEvent (TtsEventId.EndInputStream, prompt, exception, null);
}
}
else
{
// More prompts to play.
// Send a single notification that this one is over.
InjectEvent (TtsEventId.EndInputStream, prompt, exception, null);
}
}
}
}
///
/// This method is used to create the Engine voice and initialize
///
///
private TTSVoice GetVoice (VoiceInfo voiceInfo, bool switchContext)
{
TTSVoice voice = null;
lock (_voiceDictionary)
{
if (!_voiceDictionary.TryGetValue (voiceInfo, out voice))
{
if (switchContext)
{
ExecuteOnBackgroundThread (Action.GetVoice, voiceInfo);
// Voice is null if exception occured
voice = _pendingException == null ? _pendingVoice : null;
}
else
{
// Get the voice
voice = GetProxyEngine (voiceInfo);
}
}
}
return voice;
}
private void ExecuteOnBackgroundThread (Action action, object parameter)
{
//--- Get the voice on the worker thread
lock (_pendingSpeakQueue)
{
_evtPendingGetProxy.Reset ();
_pendingSpeakQueue.Enqueue (new Parameters (action, parameter));
// Start the worker thread if the list was empty
if (_pendingSpeakQueue.Count == 1)
{
_evtPendingSpeak.Set ();
}
}
_evtPendingGetProxy.WaitOne ();
}
private TTSVoice GetEngineWithVoice (TTSVoice defaultVoice, VoiceInfo defaultVoiceId, string name, CultureInfo culture, VoiceGender gender, VoiceAge age, int variant, bool switchContext)
{
TTSVoice voice = null;
// The list of enabled voices can be changed by a speech application
lock (_enabledVoicesLock)
{
// Do we have a name?
if (!string.IsNullOrEmpty (name))
{
// try to find a voice for a given name
voice = MatchVoice (name, variant, switchContext);
}
// Still no voice loop to find a matching one.
if (voice == null)
{
InstalledVoice viDefault = null;
// Easy out if the voice is the default voice
if (defaultVoice != null || defaultVoiceId != null)
{
// try to select the default voice
viDefault = InstalledVoice.Find (_installedVoices, defaultVoice != null ? defaultVoice.VoiceInfo : defaultVoiceId);
if (viDefault != null && viDefault.Enabled && variant == 1)
{
VoiceInfo vi = viDefault.VoiceInfo;
if (viDefault.Enabled && vi.Culture.Equals (culture) && (gender == VoiceGender.NotSet || gender == VoiceGender.Neutral || gender == vi.Gender) && (age == VoiceAge.NotSet || age == vi.Age))
{
voice = defaultVoice;
}
}
}
// Pick the first one in the list as the backup default
while (voice == null && _installedVoices.Count > 0)
{
if (viDefault == null)
{
viDefault = InstalledVoice.FirstEnabled (_installedVoices, CultureInfo.CurrentUICulture);
}
if (viDefault != null)
{
voice = MatchVoice (culture, gender, age, variant, switchContext, ref viDefault);
}
else
{
break;
}
}
}
//--- Create the default voice
if (voice == null)
{
if (defaultVoice == null)
{
throw new InvalidOperationException (SR.Get (SRID.SynthesizerVoiceFailed));
}
else
{
voice = defaultVoice;
}
}
}
return voice;
}
///
/// Try to find a voice for a given name
///
///
///
///
///
private TTSVoice MatchVoice (string name, int variant, bool switchContext)
{
TTSVoice voice = null;
// Look for it in the object tokens
VoiceInfo voiceInfo = null;
int cVariant = variant;
foreach (InstalledVoice sysVoice in _installedVoices)
{
int firstCharacter;
if (sysVoice.Enabled && (firstCharacter = name.IndexOf (sysVoice.VoiceInfo.Name, StringComparison.Ordinal)) >= 0)
{
int lastCharacter = firstCharacter + sysVoice.VoiceInfo.Name.Length;
if ((firstCharacter == 0 || name [firstCharacter - 1] == ' ') && (lastCharacter == name.Length || name [lastCharacter] == ' '))
{
voiceInfo = sysVoice.VoiceInfo;
if (cVariant-- == 1)
{
break;
}
}
}
}
// If we had a name, try to get engine from it
if (voiceInfo != null)
{
// Do we already have an voice for this voiceInfo?
voice = GetVoice (voiceInfo, switchContext);
}
return voice;
}
private TTSVoice MatchVoice (CultureInfo culture, VoiceGender gender, VoiceAge age, int variant, bool switchContext, ref InstalledVoice viDefault)
{
TTSVoice voice = null;
// Build a list with all the tokens
List tokens = new List (_installedVoices);
// Remove all the voices that are disabled
for (int i = tokens.Count - 1; i >= 0; i--)
{
if (!tokens [i].Enabled)
{
tokens.RemoveAt (i);
}
}
// Try to select the best available voice
for (; voice == null && tokens.Count > 0; )
{
InstalledVoice sysVoice = MatchVoice (viDefault, culture, gender, age, variant, tokens);
if (sysVoice != null)
{
// Find a voice and a match engine!
voice = GetVoice (sysVoice.VoiceInfo, switchContext);
if (voice == null)
{
// The voice associated with this token cannot be instanciated.
// Remove it from the list of posssible voices
tokens.Remove (sysVoice);
sysVoice.SetEnabledFlag (false, switchContext);
if (sysVoice == viDefault)
{
viDefault = null;
}
}
break;
}
}
return voice;
}
private static InstalledVoice MatchVoice (InstalledVoice defaultTokenInfo, CultureInfo culture, VoiceGender gender, VoiceAge age, int variant, List tokensInfo)
{
// Set the default return value
InstalledVoice sysVoice = defaultTokenInfo;
int bestMatch = CalcMatchValue (culture, gender, age, sysVoice.VoiceInfo);
int iPosDefault = -1;
// calc the best possible match
for (int iToken = 0; iToken < tokensInfo.Count; iToken++)
{
InstalledVoice ti = tokensInfo [iToken];
if (ti.Enabled)
{
int matchValue = CalcMatchValue (culture, gender, age, ti.VoiceInfo);
if (ti.Equals (defaultTokenInfo))
{
iPosDefault = iToken;
}
// Is this a better match?
if (matchValue > bestMatch)
{
sysVoice = ti;
bestMatch = matchValue;
}
// If we cannot get a better voice, exit
if (matchValue == 0x7 && (variant == 1 || iPosDefault >= 0))
{
break;
}
}
}
if (variant > 1)
{
// Set the default voice as the first entry
tokensInfo [iPosDefault] = tokensInfo [0];
tokensInfo [0] = defaultTokenInfo;
int requestedVariant = variant;
do
{
foreach (InstalledVoice ti in tokensInfo)
{
if (ti.Enabled && CalcMatchValue (culture, gender, age, ti.VoiceInfo) == bestMatch)
{
// If we are looking for a variant and are matching the best match, switch voice
--variant;
sysVoice = ti;
}
if (variant == 0)
{
break;
}
}
// if the variant number is large, calc the modulo and restart from there
if (variant > 0)
{
variant = requestedVariant % (requestedVariant - variant);
}
}
while (variant > 0);
}
return sysVoice;
}
private static int CalcMatchValue (CultureInfo culture, VoiceGender gender, VoiceAge age, VoiceInfo voiceInfo)
{
int matchValue;
if (voiceInfo != null)
{
matchValue = 0;
CultureInfo tokCulture = voiceInfo.Culture;
if (culture != null && Helpers.CompareInvariantCulture (tokCulture, culture))
{
// Exact Culture match has priority over gender and age.
if (culture.Equals (tokCulture))
{
matchValue |= 0x4;
}
// Male / Female has priority ove age
if (gender == VoiceGender.NotSet || voiceInfo.Gender == gender)
{
matchValue |= 0x2;
}
// Age check
if (age == VoiceAge.NotSet || voiceInfo.Age == age)
{
matchValue |= 0x1;
}
}
}
else
{
matchValue = -1;
}
return matchValue;
}
private TTSVoice GetProxyEngine (VoiceInfo voiceInfo)
{
// Create the TTS voice
// Try to get a managed SSML engine
ITtsEngineProxy engineProxy = GetSsmlEngine (voiceInfo);
// Try to get a COM engine
if (engineProxy == null)
{
engineProxy = GetComEngine (voiceInfo);
}
// store the proxy object
TTSVoice voice = null;
if (engineProxy != null)
{
voice = new TTSVoice (engineProxy, voiceInfo);
_voiceDictionary.Add (voiceInfo, voice);
}
#if SPEECHSERVER && !SERVERTESTDLL
if (voiceInfo.VoiceCategory == VoiceCategory.ScanSoft)
{
if (!TryScanSoftHandshake (voice))
{
voice = null;
}
}
#endif
return voice;
}
private ITtsEngineProxy GetSsmlEngine (VoiceInfo voiceInfo)
{
// Try first to get a TtsEngineSsml for it
ITtsEngineProxy engineProxy = null;
try
{
Assembly assembly;
if (!string.IsNullOrEmpty (voiceInfo.AssemblyName) && (assembly = Assembly.Load (voiceInfo.AssemblyName)) != null)
{
//
Type [] types = assembly.GetTypes ();
TtsEngineSsml ssmlEngine = null;
foreach (Type type in types)
{
if (type.IsSubclassOf (typeof (TtsEngineSsml)))
{
string [] args = new string [] { voiceInfo.Clsid };
ssmlEngine = assembly.CreateInstance (type.ToString (), false, BindingFlags.Default, null, args, CultureInfo.CurrentUICulture, null) as TtsEngineSsml;
break;
}
}
if (ssmlEngine != null)
{
// Create the engine site if not yet available
engineProxy = new TtsProxySsml (ssmlEngine, _site, voiceInfo.Culture.LCID);
}
}
}
catch (ArgumentException)
{
}
catch (IOException)
{
}
catch (BadImageFormatException)
{
}
return engineProxy;
}
private ITtsEngineProxy GetComEngine (VoiceInfo voiceInfo)
{
ITtsEngineProxy engineProxy = null;
try
{
ObjectToken token = ObjectToken.Create (null, voiceInfo.RegistryKeyPath, false);
if (token != null)
{
object engine = token.CreateObjectFromToken