//--------------------------------------------------------------------- // Static members /// <summary> /// Reads the text-to-speech engine from the application's configuration /// using the specified key prefix. /// </summary> /// <param name="keyPrefix">The application configuration key prefix.</param> /// <returns>The settings.</returns> /// <remarks> /// <para> /// The text-to-speech engine settings are loaded from the application /// configuration, using the specified key prefix. The following /// settings are recognized by the class: /// </para> /// <div class="tablediv"> /// <table class="dtTABLE" cellspacing="0" ID="Table1"> /// <tr valign="top"> /// <th width="1">Setting</th> /// <th width="1">Default</th> /// <th width="90%">Description</th> /// </tr> /// <tr valign="top"> /// <td>PhraseCacheFolder</td> /// <td><b>$(ProgramDataPath)\LillTek\NeonSwitch\PhraseCache</b></td> /// <td> /// The fully qualified path to the file system folder where the cache will be located. /// </td> /// </tr> /// <tr valign="top"> /// <td>OneTimePhraseFolder</td> /// <td><b>$(ProgramDataPath)\LillTek\NeonSwitch\PhraseCache\OneTime</b></td> /// <td> /// The fully qualified path to the file system folder where one-time /// phrases will be located while they are played by NeonSwitch. /// </td> /// </tr> /// <tr valign="top"> /// <td>PhraseFolderFanout</td> /// <td><b>100</b></td> /// <td> /// Specifies the number of subfolders to create in the cache. Cached audio /// files will be distributed randomly across these folders to avoid having a huge /// number of files in any one folder. /// </td> /// </tr> /// <tr valign="top"> /// <td>PhrasePurgeInterval</td> /// <td><b>1m</b></td> /// <td> /// The time interval at which temporary phrases as well as phrases that /// have not been referenced for some period of time will be purged. /// </td> /// </tr> /// <tr valign="top"> /// <td>MaxPhraseTTL</td> /// <td><b>1d</b></td> /// <td> /// The maximum time a phrase will be cached if it is not used. /// </td> /// </tr> /// <tr valign="top"> /// <td>MaxOneTimePhraseTTL</td> /// <td><b>5m</b></td> /// <td> /// The maximum time a one-time phrase audio file will be retained. /// </td> /// </tr> /// <tr valign="top"> /// <td>DefaultVoice</td> /// <td><b>auto</b></td> /// <td> /// <para> /// The default speech synthesis voice. This defaults to <b>auto</b> /// which has the switch choose the voice depending on which voices are /// currently installed. /// </para> /// <para> /// NeonSwitch will favor Cepstral 8KHz voices if present, especially /// <b>Cepstral Allison-8kHz</b> but fall back to <b>Microsoft Anna</b> /// which should be present on all Windows machines. /// </para> /// </td> /// </tr> /// </table> /// </div> /// </remarks> public static SpeechEngineSettings LoadConfig(string keyPrefix) { var config = new Config(keyPrefix); var settings = new SpeechEngineSettings(); settings.PhraseCacheFolder = config.Get("PhraseCacheFolder", settings.PhraseCacheFolder); settings.OneTimePhraseFolder = config.Get("OneTimePhraseFolder", settings.OneTimePhraseFolder); settings.PhraseFolderFanout = config.Get("PhraseFolderFanout", settings.PhraseFolderFanout); settings.PhrasePurgeInterval = config.Get("PhrasePurgeInterval", settings.PhrasePurgeInterval); settings.MaxPhraseTTL = config.Get("MaxPhraseTTL", settings.MaxPhraseTTL); settings.MaxOneTimePhraseTTL = config.Get("MaxOneTimePhraseTTL", settings.MaxOneTimePhraseTTL); settings.DefaultVoice = config.Get("DefaultVoice", settings.DefaultVoice); return(settings); }
/// <summary> /// Constructs and initializes the phrase cache using TTS engine /// settings passed. /// </summary> /// <param name="settings">The engine settings.</param> public PhraseCache(SpeechEngineSettings settings) { this.settings = settings; this.isRunning = true; this.index = new Dictionary <string, Phrase>(); this.purgeTimer = new PolledTimer(settings.PhrasePurgeInterval, true); this.oneTimePurgeTimer = new PolledTimer(Helper.Divide(settings.MaxOneTimePhraseTTL, 2)); // Make sure the cache folders exist. Helper.CreateFolderTree(settings.PhraseCacheFolder); Helper.CreateFolderTree(settings.OneTimePhraseFolder); for (int i = 0; i < settings.PhraseFolderFanout; i++) { Helper.CreateFolderTree(Path.Combine(settings.PhraseCacheFolder, GetSubfolderName(i))); } // Initialize the cache. PurgeOneTime(); LoadIndex(); }
private static Dictionary <string, string> voiceNameMap; // Maps friendly voice names to the internal name /// <summary> /// Starts the engine if it is not already running. /// </summary> /// <param name="settings">The engine settings.</param> /// <exception cref="ArgumentNullException">Thrown if <paramref name="settings"/> is <c>null</c>.</exception> public static void Start(SpeechEngineSettings settings) { if (settings == null) { throw new ArgumentNullException("settings"); } lock (syncLock) { if (SpeechEngine.isRunning) { return; } SpeechEngine.settings = settings; SpeechEngine.cache = new PhraseCache(settings); SpeechEngine.isRunning = true; // Create the default audio formats. format_8000KHz = new SpeechAudioFormatInfo(8000, AudioBitsPerSample.Eight, AudioChannel.Mono); format_11025KHz = new SpeechAudioFormatInfo(11025, AudioBitsPerSample.Eight, AudioChannel.Mono); format_16000KHz = new SpeechAudioFormatInfo(16000, AudioBitsPerSample.Eight, AudioChannel.Mono); // Get the fully qualified paths to the error files. noVoicesPath = Path.Combine(CoreApp.InstallPath, "Audio", "NoVoicesError.wav"); synthErrorPath = Path.Combine(CoreApp.InstallPath, "Audio", "SpeechSynthError.wav"); // Enumerate the installed voices and select the default voice. // // Note: The Microsoft Speech Platform voices have really clunky names like: // // "Microsoft Server Speech Text to Speech Voice (en-AU, Hayley)" // // I'm going to simplify these to be just "Microsoft <name>" and maintain // a table that maps back to the original name. voiceNameMap = new Dictionary <string, string>(StringComparer.OrdinalIgnoreCase); using (var synth = new SpeechSynthesizer()) { var voices = new Dictionary <string, VoiceInfo>(StringComparer.OrdinalIgnoreCase); foreach (var voice in synth.GetInstalledVoices()) { var voiceName = voice.VoiceInfo.Name; if (!voice.Enabled) { continue; } // $hack(jeff.lill): // // Make sure that the voice can actually be used. I've run into // situations where [Microsoft Anna] was present and enabled but // could not be selected. I believe this may be a 64-bit issue // or perhaps installing Cepstral voices messes with Anna. try { synth.SelectVoice(voice.VoiceInfo.Name); } catch { continue; } if (voiceName.StartsWith("Microsoft Server Speech Text to Speech Voice (")) { int p = voiceName.IndexOf(','); if (p != -1) { voiceName = voiceName.Substring(p + 1); voiceName = "Microsoft " + voiceName.Replace(")", string.Empty).Trim(); voiceNameMap[voiceName] = voice.VoiceInfo.Name; } } voices.Add(voiceName, voice.VoiceInfo); } SpeechEngine.InstalledVoices = voices.ToReadOnly(); SpeechEngine.DefaultVoice = null; SpeechEngine.DefaultVoiceInfo = null; // First see if the desired default voice exists. if (!string.IsNullOrWhiteSpace(settings.DefaultVoice) && String.Compare(settings.DefaultVoice, "auto") != 0) { VoiceInfo voiceInfo; if (voices.TryGetValue(settings.DefaultVoice, out voiceInfo)) { SpeechEngine.DefaultVoice = voiceInfo.Name; } else { SysLog.LogWarning("[SpeechEngine] was not able to locate the requested default voice [{0}]. Another voice will be selected automatically.", settings.DefaultVoice); } } // If not look for an alternative if (SpeechEngine.DefaultVoice == null) { if (voices.ContainsKey("Microsoft Helen")) { SpeechEngine.DefaultVoice = "Microsoft Helen"; } else if (voices.ContainsKey("Microsoft Anna")) { SpeechEngine.DefaultVoice = "Microsoft Anna"; } else { SysLog.LogWarning("[SpeechEngine] was not able to locate the [Microsoft Anna] voice."); var v = voices.Keys.FirstOrDefault(); if (v == null) { SpeechEngine.DefaultVoice = null; SysLog.LogError("[SpeechEngine] was not able to locate any speech synthesis voices. Speech synthesis will be disabled."); } else { SpeechEngine.DefaultVoice = v; } } } if (SpeechEngine.DefaultVoice != null) { SpeechEngine.DefaultVoiceInfo = SpeechEngine.InstalledVoices[GetVoice(SpeechEngine.DefaultVoice)]; } } } }
/// <summary> /// Starts the service, associating it with an <see cref="IServiceHost" /> instance. /// </summary> /// <param name="serviceHost">The service user interface.</param> /// <param name="args">Command line arguments.</param> public void Start(IServiceHost serviceHost, string[] args) { lock (syncLock) { if (router != null) { return; // Already started } // Global initialization startTime = DateTime.UtcNow; NetTrace.Start(); CoreApp.InstallPerfCounters(); // Service initialization this.serviceHost = serviceHost; try { SysLog.LogInformation("NeonSwitch v{0} Start", Helper.GetVersionString()); router = new LeafRouter(); router.Start(); state = ServiceState.Running; bkTimer = new GatedTimer(OnBkTimer, null, CoreApp.BkTaskInterval); SpeechEngine.Start(SpeechEngineSettings.LoadConfig("NeonSwitch.Speech")); #if DEBUG // $todo(jeff.lill): Delete this. SwitchTest.Test(); #endif // Indicate that the switch core service is open for business. NeonSwitch // application instance loaders will spin, waiting for this to be set before // calling the application's main function. Switch.SetGlobal(SwitchGlobal.NeonSwitchReady, "true"); } catch (Exception e) { SpeechEngine.Stop(); if (bkTimer != null) { bkTimer.Dispose(); bkTimer = null; } if (router != null) { router.Stop(); router = null; } SysLog.LogException(e); throw; } } }