From 111a232df2d2507a68a8d7206f2dcc7697ff159f Mon Sep 17 00:00:00 2001 From: Dan Moseley Date: Thu, 7 Jan 2021 19:04:13 -0800 Subject: [PATCH 1/9] Port System.Speech to .NET Core (#45941) * Initial sources, with banners * Run the code formatter * Fix hang in XUnit due to failing to complete all AsyncOperation-s * Remove reflection over RegistryKey * Add ref and packaging * Add tests * Add sln * Fix CS1584 * Fix CA1823 * Fix CA1834 * Unnecessary suppressions * Fix SA1028 * Fix CA1507 * Fix CA1810 * Fix CA1825 * Fix CA1825 * Unnecessary suppressions * Fix CA1805 * Fix IDE0004 * Fix IDE0090 * Remove CAS * Remove tabs and dead code * Unnecessary suppressions * Fix SA1212 * Fix SA1121 * Disable SA1129 * Fix SA1206 * Fix SA1518 * Fix SA1617 * Fix SA1001 * Fix CS0618 * Remove unnecessary comments * Remove unnecessary whitespace * Remove low value xml doc comments * Unused usings * dead files * Remove CAS * More junk * Fix obvious original bug * Remove/insert newlines * Remove reference to old design document * Fix spacing * Fix typo name * Fix file casing * Remove dead code * Add to compat pack * Remove AppDomain etc * Fix casing of .NET * Remove low value XML docs * Remove code that relies on compiling assemblies * Fix inadvertently removed padding * Use EDI to preserve stack when rethrowing * Fix misaligned resource ID's to match sperror.h * Skip SpeechRecognitionEngine tests if no installed recognizers * Fix misformatted string bug * Logging for CI error * Fix NRE trying to map phonemes for voice for culture for which we do not have phoneme map * Fix 153 spelling errors in comments using `Visual Studio Spell Checker` * Remove extraneous file * Fix spacing * Fix project reference * Reorder properties in csproj * Change from netcoreapp2.0 to netcoreapp2.1 * Update src/libraries/System.Speech/pkg/System.Speech.pkgproj Co-authored-by: Jose Perez Rodriguez * Add build error for targeting netcoreapp2.0 * Suppress new error during packaging testing * Update System.Speech.targets * Remove ref comments * Update pkgproj * Remove placeholder Co-authored-by: Jose Perez Rodriguez Co-authored-by: Viktor Hofer --- src/libraries/Common/src/System/SR.cs | 1 - .../Runtime/Versioning/PlatformAttributes.cs | 4 +- .../System.Speech/Directory.Build.props | 16 + src/libraries/System.Speech/System.Speech.sln | 31 + .../System.Speech/pkg/System.Speech.pkgproj | 16 + .../pkg/build/System.Speech.targets | 7 + .../System.Speech/ref/System.Speech.cs | 1157 ++++++ .../System.Speech/ref/System.Speech.csproj | 8 + .../src/AudioFormat/AudioFormatConverter.cs | 300 ++ .../src/AudioFormat/EncodingFormat.cs | 13 + .../src/AudioFormat/SpeechAudioFormatInfo.cs | 192 + .../src/Internal/AlphabetConverter.cs | 342 ++ .../src/Internal/AsyncSerializedWorker.cs | 282 ++ .../GrammarBuilding/BuilderElements.cs | 274 ++ .../GrammarBuilding/GrammarBuilderBase.cs | 91 + .../GrammarBuilderDictation.cs | 94 + .../GrammarBuilding/GrammarBuilderPhrase.cs | 133 + .../GrammarBuilding/GrammarBuilderRuleRef.cs | 75 + .../GrammarBuilding/GrammarBuilderWildcard.cs | 63 + .../GrammarBuilding/IdentifierCollection.cs | 52 + .../Internal/GrammarBuilding/ItemElement.cs | 107 + .../Internal/GrammarBuilding/OneOfElement.cs | 73 + .../Internal/GrammarBuilding/RuleElement.cs | 121 + .../GrammarBuilding/RuleRefElement.cs | 96 + .../GrammarBuilding/SemanticKeyElement.cs | 102 + .../Internal/GrammarBuilding/TagElement.cs | 111 + .../src/Internal/HGlobalSafeHandle.cs | 101 + .../System.Speech/src/Internal/Helpers.cs | 115 + .../src/Internal/ObjectToken/ObjectToken.cs | 334 ++ .../ObjectToken/ObjectTokenCategory.cs | 103 + .../Internal/ObjectToken/RegistryDataKey.cs | 539 +++ .../Internal/ObjectToken/SAPICategories.cs | 329 ++ .../src/Internal/PhonemeConverter.cs | 280 ++ .../src/Internal/RedBlackList.cs | 728 ++++ .../src/Internal/ResourceLoader.cs | 114 + .../src/Internal/SapiAttributeParser.cs | 57 + .../src/Internal/SapiInterop/EventNotify.cs | 121 + .../Internal/SapiInterop/SapiEventInterop.cs | 144 + .../src/Internal/SapiInterop/SapiGrammar.cs | 118 + .../src/Internal/SapiInterop/SapiInterop.cs | 287 ++ .../src/Internal/SapiInterop/SapiProxy.cs | 278 ++ .../Internal/SapiInterop/SapiRecoContext.cs | 95 + .../Internal/SapiInterop/SapiRecoInterop.cs | 1053 ++++++ .../Internal/SapiInterop/SapiRecognizer.cs | 263 ++ .../Internal/SapiInterop/SapiStreamInterop.cs | 71 + .../SapiInterop/SpAudioStreamWrapper.cs | 181 + .../Internal/SapiInterop/SpStreamWrapper.cs | 118 + .../src/Internal/SapiInterop/SpeechEvent.cs | 172 + .../src/Internal/SeekableReadStream.cs | 245 ++ .../SrgsCompiler/AppDomainGrammarProxy.cs | 316 ++ .../src/Internal/SrgsCompiler/Arc.cs | 875 +++++ .../src/Internal/SrgsCompiler/ArcList.cs | 92 + .../src/Internal/SrgsCompiler/BackEnd.cs | 1394 +++++++ .../src/Internal/SrgsCompiler/CFGGrammar.cs | 574 +++ .../src/Internal/SrgsCompiler/CfgArc.cs | 176 + .../src/Internal/SrgsCompiler/CfgRule.cs | 233 ++ .../src/Internal/SrgsCompiler/CfgScriptRef.cs | 23 + .../Internal/SrgsCompiler/CfgSemanticTag.cs | 207 ++ .../Internal/SrgsCompiler/CustomGrammar.cs | 172 + .../Internal/SrgsCompiler/GrammarElement.cs | 370 ++ .../src/Internal/SrgsCompiler/Graph.cs | 995 +++++ .../src/Internal/SrgsCompiler/Item.cs | 161 + .../src/Internal/SrgsCompiler/OneOf.cs | 109 + .../src/Internal/SrgsCompiler/ParseElement.cs | 52 + .../SrgsCompiler/ParseElementCollection.cs | 307 ++ .../src/Internal/SrgsCompiler/PropertyTag.cs | 97 + .../src/Internal/SrgsCompiler/Rule.cs | 307 ++ .../src/Internal/SrgsCompiler/RuleRef.cs | 219 ++ .../src/Internal/SrgsCompiler/SRGSCompiler.cs | 200 + .../src/Internal/SrgsCompiler/ScriptRef.cs | 71 + .../src/Internal/SrgsCompiler/SemanticTag.cs | 50 + .../SrgsElementCompilerFactory.cs | 360 ++ .../src/Internal/SrgsCompiler/State.cs | 507 +++ .../src/Internal/SrgsCompiler/Subset.cs | 48 + .../src/Internal/SrgsCompiler/Tag.cs | 61 + .../src/Internal/SrgsParser/IElement.cs | 13 + .../Internal/SrgsParser/IElementFactory.cs | 39 + .../src/Internal/SrgsParser/IElementText.cs | 12 + .../src/Internal/SrgsParser/IGrammar.cs | 36 + .../src/Internal/SrgsParser/IItem.cs | 12 + .../src/Internal/SrgsParser/IOneOf.cs | 12 + .../src/Internal/SrgsParser/IPropertyTag.cs | 13 + .../src/Internal/SrgsParser/IRule.cs | 30 + .../src/Internal/SrgsParser/IRuleRef.cs | 12 + .../src/Internal/SrgsParser/IScript.cs | 21 + .../src/Internal/SrgsParser/ISemanticTag.cs | 13 + .../src/Internal/SrgsParser/ISrgsParser.cs | 11 + .../src/Internal/SrgsParser/ISubset.cs | 22 + .../src/Internal/SrgsParser/IToken.cs | 17 + .../Internal/SrgsParser/SrgsDocumentParser.cs | 423 +++ .../src/Internal/SrgsParser/XmlParser.cs | 1922 ++++++++++ .../src/Internal/StreamMarshaler.cs | 173 + .../System.Speech/src/Internal/StringBlob.cs | 219 ++ .../src/Internal/Synthesis/AudioBase.cs | 454 +++ .../src/Internal/Synthesis/AudioDeviceOut.cs | 509 +++ .../src/Internal/Synthesis/AudioException.cs | 24 + .../src/Internal/Synthesis/AudioFileOut.cs | 261 ++ .../Synthesis/AudioFormatConverter.cs | 612 ++++ .../src/Internal/Synthesis/ConvertTextFrag.cs | 440 +++ .../src/Internal/Synthesis/EngineSite.cs | 542 +++ .../src/Internal/Synthesis/EngineSiteSapi.cs | 212 ++ .../src/Internal/Synthesis/ISSmlParser.cs | 108 + .../src/Internal/Synthesis/PcmConverter.cs | 466 +++ .../src/Internal/Synthesis/SSmlParser.cs | 2154 +++++++++++ .../Internal/Synthesis/SafeNativeMethods.cs | 216 ++ .../src/Internal/Synthesis/SpeakInfo.cs | 167 + .../src/Internal/Synthesis/SpeechSeg.cs | 83 + .../src/Internal/Synthesis/TTSEngineProxy.cs | 212 ++ .../src/Internal/Synthesis/TTSEvent.cs | 178 + .../src/Internal/Synthesis/TTSVoice.cs | 158 + .../Internal/Synthesis/TextFragmentEngine.cs | 321 ++ .../Internal/Synthesis/TextWriterEngine.cs | 385 ++ .../src/Internal/Synthesis/VoiceSynthesis.cs | 1853 ++++++++++ .../src/Internal/Synthesis/WaveHeader.cs | 154 + .../Recognition/AudioLevelUpdatedEventArgs.cs | 33 + .../src/Recognition/AudioSignalProblem.cs | 29 + .../AudioSignalProblemOccurredEventArgs.cs | 51 + .../src/Recognition/AudioState.cs | 18 + .../Recognition/AudioStateChangedEventArgs.cs | 33 + .../System.Speech/src/Recognition/Choices.cs | 85 + .../src/Recognition/DictationGrammar.cs | 57 + .../EmulateRecognizeCompletedEventArgs.cs | 34 + .../System.Speech/src/Recognition/Grammar.cs | 1166 ++++++ .../src/Recognition/GrammarBuilder.cs | 534 +++ .../src/Recognition/IRecognizerInternal.cs | 23 + .../LoadGrammarCompletedEventArgs.cs | 39 + .../RecognizeCompletedEventArgs.cs | 60 + .../src/Recognition/RecognizeMode.cs | 11 + .../src/Recognition/RecognizerBase.cs | 3255 +++++++++++++++++ .../src/Recognition/RecognizerInfo.cs | 156 + .../src/Recognition/RecognizerState.cs | 15 + .../RecognizerStateChangedEventArgs.cs | 33 + .../src/Recognition/SemanticResultKey.cs | 77 + .../src/Recognition/SemanticResultValue.cs | 63 + .../Recognition/SpeechDetectedEventArgs.cs | 33 + .../Recognition/SpeechRecognitionEngine.cs | 690 ++++ .../src/Recognition/SpeechRecognizer.cs | 501 +++ .../System.Speech/src/Recognition/SpeechUI.cs | 21 + .../Recognition/SrgsGrammar/SrgsDocument.cs | 425 +++ .../Recognition/SrgsGrammar/SrgsElement.cs | 87 + .../SrgsGrammar/SrgsElementFactory.cs | 221 ++ .../SrgsGrammar/SrgsElementList.cs | 23 + .../Recognition/SrgsGrammar/SrgsGrammar.cs | 698 ++++ .../SrgsGrammar/SrgsGrammarCompiler.cs | 158 + .../src/Recognition/SrgsGrammar/SrgsItem.cs | 396 ++ .../Recognition/SrgsGrammar/SrgsItemList.cs | 23 + .../SrgsGrammar/SrgsNameValueTag.cs | 197 + .../src/Recognition/SrgsGrammar/SrgsOneOf.cs | 158 + .../src/Recognition/SrgsGrammar/SrgsRule.cs | 537 +++ .../Recognition/SrgsGrammar/SrgsRuleRef.cs | 297 ++ .../SrgsGrammar/SrgsRulesCollection.cs | 34 + .../SrgsSemanticInterpretationTag.cs | 102 + .../src/Recognition/SrgsGrammar/SrgsSubset.cs | 136 + .../Recognition/SrgsGrammar/SrgsTagFormat.cs | 16 + .../src/Recognition/SrgsGrammar/SrgsText.cs | 71 + .../src/Recognition/SrgsGrammar/SrgsToken.cs | 169 + .../src/Recognition/SubsetMatchingMode.cs | 16 + .../src/Recognition/UpdateEventArgs.cs | 41 + .../System.Speech/src/Resources/Strings.resx | 1273 +++++++ .../src/Result/RecognitionEventArgs.cs | 64 + .../src/Result/RecognitionResult.cs | 553 +++ .../src/Result/RecognizedAudio.cs | 156 + .../src/Result/RecognizedPhrase.cs | 1226 +++++++ .../src/Result/RecognizedWordUnit.cs | 115 + .../src/Result/ReplacementText.cs | 66 + .../System.Speech/src/Result/SemanticValue.cs | 265 ++ src/libraries/System.Speech/src/SR.cs | 27 + src/libraries/System.Speech/src/SRID.cs | 406 ++ .../src/Synthesis/BookmarkEventArgs.cs | 45 + .../System.Speech/src/Synthesis/FilePrompt.cs | 22 + .../src/Synthesis/InstalledVoice.cs | 143 + .../src/Synthesis/PhonemeEventArgs.cs | 63 + .../System.Speech/src/Synthesis/Prompt.cs | 170 + .../src/Synthesis/PromptBuilder.cs | 1098 ++++++ .../src/Synthesis/PromptEventArgs.cs | 47 + .../src/Synthesis/PromptStyle.cs | 153 + .../src/Synthesis/SpeakCompletedEventArgs.cs | 15 + .../src/Synthesis/SpeakProgressEventArgs.cs | 66 + .../src/Synthesis/SpeechSynthesizer.cs | 555 +++ .../SynthesizerStateChangedEventArgs.cs | 45 + .../Synthesis/TTSEngine/SAPIEngineTypes.cs | 128 + .../src/Synthesis/TTSEngine/TTSEngineTypes.cs | 536 +++ .../src/Synthesis/VisemeEventArgs.cs | 63 + .../src/Synthesis/VoiceChangeEventArgs.cs | 33 + .../System.Speech/src/Synthesis/VoiceInfo.cs | 297 ++ .../System.Speech/src/System.Speech.csproj | 258 ++ .../System.Speech/src/upstable_chs.upsmap | Bin 0 -> 118766 bytes .../System.Speech/src/upstable_cht.upsmap | Bin 0 -> 8980 bytes .../System.Speech/src/upstable_deu.upsmap | Bin 0 -> 4512 bytes .../System.Speech/src/upstable_enu.upsmap | Bin 0 -> 3244 bytes .../System.Speech/src/upstable_esp.upsmap | Bin 0 -> 4782 bytes .../System.Speech/src/upstable_fra.upsmap | Bin 0 -> 3874 bytes .../System.Speech/src/upstable_jpn.upsmap | Bin 0 -> 11214 bytes .../System.Speech/tests/GrammarTests.cs | 175 + .../System.Speech/tests/OtherTests.cs | 66 + .../tests/SpeechRecognizerTests.cs | 63 + .../tests/SynthesizeRecognizeTests.cs | 191 + .../tests/System.Speech.Tests.csproj | 15 + .../Microsoft.Windows.Compatibility.pkgproj | 3 +- src/libraries/pkg/baseline/packageIndex.json | 4 + .../pkg/test/project.csproj.template | 2 + 201 files changed, 49317 insertions(+), 4 deletions(-) create mode 100644 src/libraries/System.Speech/Directory.Build.props create mode 100644 src/libraries/System.Speech/System.Speech.sln create mode 100644 src/libraries/System.Speech/pkg/System.Speech.pkgproj create mode 100644 src/libraries/System.Speech/pkg/build/System.Speech.targets create mode 100644 src/libraries/System.Speech/ref/System.Speech.cs create mode 100644 src/libraries/System.Speech/ref/System.Speech.csproj create mode 100644 src/libraries/System.Speech/src/AudioFormat/AudioFormatConverter.cs create mode 100644 src/libraries/System.Speech/src/AudioFormat/EncodingFormat.cs create mode 100644 src/libraries/System.Speech/src/AudioFormat/SpeechAudioFormatInfo.cs create mode 100644 src/libraries/System.Speech/src/Internal/AlphabetConverter.cs create mode 100644 src/libraries/System.Speech/src/Internal/AsyncSerializedWorker.cs create mode 100644 src/libraries/System.Speech/src/Internal/GrammarBuilding/BuilderElements.cs create mode 100644 src/libraries/System.Speech/src/Internal/GrammarBuilding/GrammarBuilderBase.cs create mode 100644 src/libraries/System.Speech/src/Internal/GrammarBuilding/GrammarBuilderDictation.cs create mode 100644 src/libraries/System.Speech/src/Internal/GrammarBuilding/GrammarBuilderPhrase.cs create mode 100644 src/libraries/System.Speech/src/Internal/GrammarBuilding/GrammarBuilderRuleRef.cs create mode 100644 src/libraries/System.Speech/src/Internal/GrammarBuilding/GrammarBuilderWildcard.cs create mode 100644 src/libraries/System.Speech/src/Internal/GrammarBuilding/IdentifierCollection.cs create mode 100644 src/libraries/System.Speech/src/Internal/GrammarBuilding/ItemElement.cs create mode 100644 src/libraries/System.Speech/src/Internal/GrammarBuilding/OneOfElement.cs create mode 100644 src/libraries/System.Speech/src/Internal/GrammarBuilding/RuleElement.cs create mode 100644 src/libraries/System.Speech/src/Internal/GrammarBuilding/RuleRefElement.cs create mode 100644 src/libraries/System.Speech/src/Internal/GrammarBuilding/SemanticKeyElement.cs create mode 100644 src/libraries/System.Speech/src/Internal/GrammarBuilding/TagElement.cs create mode 100644 src/libraries/System.Speech/src/Internal/HGlobalSafeHandle.cs create mode 100644 src/libraries/System.Speech/src/Internal/Helpers.cs create mode 100644 src/libraries/System.Speech/src/Internal/ObjectToken/ObjectToken.cs create mode 100644 src/libraries/System.Speech/src/Internal/ObjectToken/ObjectTokenCategory.cs create mode 100644 src/libraries/System.Speech/src/Internal/ObjectToken/RegistryDataKey.cs create mode 100644 src/libraries/System.Speech/src/Internal/ObjectToken/SAPICategories.cs create mode 100644 src/libraries/System.Speech/src/Internal/PhonemeConverter.cs create mode 100644 src/libraries/System.Speech/src/Internal/RedBlackList.cs create mode 100644 src/libraries/System.Speech/src/Internal/ResourceLoader.cs create mode 100644 src/libraries/System.Speech/src/Internal/SapiAttributeParser.cs create mode 100644 src/libraries/System.Speech/src/Internal/SapiInterop/EventNotify.cs create mode 100644 src/libraries/System.Speech/src/Internal/SapiInterop/SapiEventInterop.cs create mode 100644 src/libraries/System.Speech/src/Internal/SapiInterop/SapiGrammar.cs create mode 100644 src/libraries/System.Speech/src/Internal/SapiInterop/SapiInterop.cs create mode 100644 src/libraries/System.Speech/src/Internal/SapiInterop/SapiProxy.cs create mode 100644 src/libraries/System.Speech/src/Internal/SapiInterop/SapiRecoContext.cs create mode 100644 src/libraries/System.Speech/src/Internal/SapiInterop/SapiRecoInterop.cs create mode 100644 src/libraries/System.Speech/src/Internal/SapiInterop/SapiRecognizer.cs create mode 100644 src/libraries/System.Speech/src/Internal/SapiInterop/SapiStreamInterop.cs create mode 100644 src/libraries/System.Speech/src/Internal/SapiInterop/SpAudioStreamWrapper.cs create mode 100644 src/libraries/System.Speech/src/Internal/SapiInterop/SpStreamWrapper.cs create mode 100644 src/libraries/System.Speech/src/Internal/SapiInterop/SpeechEvent.cs create mode 100644 src/libraries/System.Speech/src/Internal/SeekableReadStream.cs create mode 100644 src/libraries/System.Speech/src/Internal/SrgsCompiler/AppDomainGrammarProxy.cs create mode 100644 src/libraries/System.Speech/src/Internal/SrgsCompiler/Arc.cs create mode 100644 src/libraries/System.Speech/src/Internal/SrgsCompiler/ArcList.cs create mode 100644 src/libraries/System.Speech/src/Internal/SrgsCompiler/BackEnd.cs create mode 100644 src/libraries/System.Speech/src/Internal/SrgsCompiler/CFGGrammar.cs create mode 100644 src/libraries/System.Speech/src/Internal/SrgsCompiler/CfgArc.cs create mode 100644 src/libraries/System.Speech/src/Internal/SrgsCompiler/CfgRule.cs create mode 100644 src/libraries/System.Speech/src/Internal/SrgsCompiler/CfgScriptRef.cs create mode 100644 src/libraries/System.Speech/src/Internal/SrgsCompiler/CfgSemanticTag.cs create mode 100644 src/libraries/System.Speech/src/Internal/SrgsCompiler/CustomGrammar.cs create mode 100644 src/libraries/System.Speech/src/Internal/SrgsCompiler/GrammarElement.cs create mode 100644 src/libraries/System.Speech/src/Internal/SrgsCompiler/Graph.cs create mode 100644 src/libraries/System.Speech/src/Internal/SrgsCompiler/Item.cs create mode 100644 src/libraries/System.Speech/src/Internal/SrgsCompiler/OneOf.cs create mode 100644 src/libraries/System.Speech/src/Internal/SrgsCompiler/ParseElement.cs create mode 100644 src/libraries/System.Speech/src/Internal/SrgsCompiler/ParseElementCollection.cs create mode 100644 src/libraries/System.Speech/src/Internal/SrgsCompiler/PropertyTag.cs create mode 100644 src/libraries/System.Speech/src/Internal/SrgsCompiler/Rule.cs create mode 100644 src/libraries/System.Speech/src/Internal/SrgsCompiler/RuleRef.cs create mode 100644 src/libraries/System.Speech/src/Internal/SrgsCompiler/SRGSCompiler.cs create mode 100644 src/libraries/System.Speech/src/Internal/SrgsCompiler/ScriptRef.cs create mode 100644 src/libraries/System.Speech/src/Internal/SrgsCompiler/SemanticTag.cs create mode 100644 src/libraries/System.Speech/src/Internal/SrgsCompiler/SrgsElementCompilerFactory.cs create mode 100644 src/libraries/System.Speech/src/Internal/SrgsCompiler/State.cs create mode 100644 src/libraries/System.Speech/src/Internal/SrgsCompiler/Subset.cs create mode 100644 src/libraries/System.Speech/src/Internal/SrgsCompiler/Tag.cs create mode 100644 src/libraries/System.Speech/src/Internal/SrgsParser/IElement.cs create mode 100644 src/libraries/System.Speech/src/Internal/SrgsParser/IElementFactory.cs create mode 100644 src/libraries/System.Speech/src/Internal/SrgsParser/IElementText.cs create mode 100644 src/libraries/System.Speech/src/Internal/SrgsParser/IGrammar.cs create mode 100644 src/libraries/System.Speech/src/Internal/SrgsParser/IItem.cs create mode 100644 src/libraries/System.Speech/src/Internal/SrgsParser/IOneOf.cs create mode 100644 src/libraries/System.Speech/src/Internal/SrgsParser/IPropertyTag.cs create mode 100644 src/libraries/System.Speech/src/Internal/SrgsParser/IRule.cs create mode 100644 src/libraries/System.Speech/src/Internal/SrgsParser/IRuleRef.cs create mode 100644 src/libraries/System.Speech/src/Internal/SrgsParser/IScript.cs create mode 100644 src/libraries/System.Speech/src/Internal/SrgsParser/ISemanticTag.cs create mode 100644 src/libraries/System.Speech/src/Internal/SrgsParser/ISrgsParser.cs create mode 100644 src/libraries/System.Speech/src/Internal/SrgsParser/ISubset.cs create mode 100644 src/libraries/System.Speech/src/Internal/SrgsParser/IToken.cs create mode 100644 src/libraries/System.Speech/src/Internal/SrgsParser/SrgsDocumentParser.cs create mode 100644 src/libraries/System.Speech/src/Internal/SrgsParser/XmlParser.cs create mode 100644 src/libraries/System.Speech/src/Internal/StreamMarshaler.cs create mode 100644 src/libraries/System.Speech/src/Internal/StringBlob.cs create mode 100644 src/libraries/System.Speech/src/Internal/Synthesis/AudioBase.cs create mode 100644 src/libraries/System.Speech/src/Internal/Synthesis/AudioDeviceOut.cs create mode 100644 src/libraries/System.Speech/src/Internal/Synthesis/AudioException.cs create mode 100644 src/libraries/System.Speech/src/Internal/Synthesis/AudioFileOut.cs create mode 100644 src/libraries/System.Speech/src/Internal/Synthesis/AudioFormatConverter.cs create mode 100644 src/libraries/System.Speech/src/Internal/Synthesis/ConvertTextFrag.cs create mode 100644 src/libraries/System.Speech/src/Internal/Synthesis/EngineSite.cs create mode 100644 src/libraries/System.Speech/src/Internal/Synthesis/EngineSiteSapi.cs create mode 100644 src/libraries/System.Speech/src/Internal/Synthesis/ISSmlParser.cs create mode 100644 src/libraries/System.Speech/src/Internal/Synthesis/PcmConverter.cs create mode 100644 src/libraries/System.Speech/src/Internal/Synthesis/SSmlParser.cs create mode 100644 src/libraries/System.Speech/src/Internal/Synthesis/SafeNativeMethods.cs create mode 100644 src/libraries/System.Speech/src/Internal/Synthesis/SpeakInfo.cs create mode 100644 src/libraries/System.Speech/src/Internal/Synthesis/SpeechSeg.cs create mode 100644 src/libraries/System.Speech/src/Internal/Synthesis/TTSEngineProxy.cs create mode 100644 src/libraries/System.Speech/src/Internal/Synthesis/TTSEvent.cs create mode 100644 src/libraries/System.Speech/src/Internal/Synthesis/TTSVoice.cs create mode 100644 src/libraries/System.Speech/src/Internal/Synthesis/TextFragmentEngine.cs create mode 100644 src/libraries/System.Speech/src/Internal/Synthesis/TextWriterEngine.cs create mode 100644 src/libraries/System.Speech/src/Internal/Synthesis/VoiceSynthesis.cs create mode 100644 src/libraries/System.Speech/src/Internal/Synthesis/WaveHeader.cs create mode 100644 src/libraries/System.Speech/src/Recognition/AudioLevelUpdatedEventArgs.cs create mode 100644 src/libraries/System.Speech/src/Recognition/AudioSignalProblem.cs create mode 100644 src/libraries/System.Speech/src/Recognition/AudioSignalProblemOccurredEventArgs.cs create mode 100644 src/libraries/System.Speech/src/Recognition/AudioState.cs create mode 100644 src/libraries/System.Speech/src/Recognition/AudioStateChangedEventArgs.cs create mode 100644 src/libraries/System.Speech/src/Recognition/Choices.cs create mode 100644 src/libraries/System.Speech/src/Recognition/DictationGrammar.cs create mode 100644 src/libraries/System.Speech/src/Recognition/EmulateRecognizeCompletedEventArgs.cs create mode 100644 src/libraries/System.Speech/src/Recognition/Grammar.cs create mode 100644 src/libraries/System.Speech/src/Recognition/GrammarBuilder.cs create mode 100644 src/libraries/System.Speech/src/Recognition/IRecognizerInternal.cs create mode 100644 src/libraries/System.Speech/src/Recognition/LoadGrammarCompletedEventArgs.cs create mode 100644 src/libraries/System.Speech/src/Recognition/RecognizeCompletedEventArgs.cs create mode 100644 src/libraries/System.Speech/src/Recognition/RecognizeMode.cs create mode 100644 src/libraries/System.Speech/src/Recognition/RecognizerBase.cs create mode 100644 src/libraries/System.Speech/src/Recognition/RecognizerInfo.cs create mode 100644 src/libraries/System.Speech/src/Recognition/RecognizerState.cs create mode 100644 src/libraries/System.Speech/src/Recognition/RecognizerStateChangedEventArgs.cs create mode 100644 src/libraries/System.Speech/src/Recognition/SemanticResultKey.cs create mode 100644 src/libraries/System.Speech/src/Recognition/SemanticResultValue.cs create mode 100644 src/libraries/System.Speech/src/Recognition/SpeechDetectedEventArgs.cs create mode 100644 src/libraries/System.Speech/src/Recognition/SpeechRecognitionEngine.cs create mode 100644 src/libraries/System.Speech/src/Recognition/SpeechRecognizer.cs create mode 100644 src/libraries/System.Speech/src/Recognition/SpeechUI.cs create mode 100644 src/libraries/System.Speech/src/Recognition/SrgsGrammar/SrgsDocument.cs create mode 100644 src/libraries/System.Speech/src/Recognition/SrgsGrammar/SrgsElement.cs create mode 100644 src/libraries/System.Speech/src/Recognition/SrgsGrammar/SrgsElementFactory.cs create mode 100644 src/libraries/System.Speech/src/Recognition/SrgsGrammar/SrgsElementList.cs create mode 100644 src/libraries/System.Speech/src/Recognition/SrgsGrammar/SrgsGrammar.cs create mode 100644 src/libraries/System.Speech/src/Recognition/SrgsGrammar/SrgsGrammarCompiler.cs create mode 100644 src/libraries/System.Speech/src/Recognition/SrgsGrammar/SrgsItem.cs create mode 100644 src/libraries/System.Speech/src/Recognition/SrgsGrammar/SrgsItemList.cs create mode 100644 src/libraries/System.Speech/src/Recognition/SrgsGrammar/SrgsNameValueTag.cs create mode 100644 src/libraries/System.Speech/src/Recognition/SrgsGrammar/SrgsOneOf.cs create mode 100644 src/libraries/System.Speech/src/Recognition/SrgsGrammar/SrgsRule.cs create mode 100644 src/libraries/System.Speech/src/Recognition/SrgsGrammar/SrgsRuleRef.cs create mode 100644 src/libraries/System.Speech/src/Recognition/SrgsGrammar/SrgsRulesCollection.cs create mode 100644 src/libraries/System.Speech/src/Recognition/SrgsGrammar/SrgsSemanticInterpretationTag.cs create mode 100644 src/libraries/System.Speech/src/Recognition/SrgsGrammar/SrgsSubset.cs create mode 100644 src/libraries/System.Speech/src/Recognition/SrgsGrammar/SrgsTagFormat.cs create mode 100644 src/libraries/System.Speech/src/Recognition/SrgsGrammar/SrgsText.cs create mode 100644 src/libraries/System.Speech/src/Recognition/SrgsGrammar/SrgsToken.cs create mode 100644 src/libraries/System.Speech/src/Recognition/SubsetMatchingMode.cs create mode 100644 src/libraries/System.Speech/src/Recognition/UpdateEventArgs.cs create mode 100644 src/libraries/System.Speech/src/Resources/Strings.resx create mode 100644 src/libraries/System.Speech/src/Result/RecognitionEventArgs.cs create mode 100644 src/libraries/System.Speech/src/Result/RecognitionResult.cs create mode 100644 src/libraries/System.Speech/src/Result/RecognizedAudio.cs create mode 100644 src/libraries/System.Speech/src/Result/RecognizedPhrase.cs create mode 100644 src/libraries/System.Speech/src/Result/RecognizedWordUnit.cs create mode 100644 src/libraries/System.Speech/src/Result/ReplacementText.cs create mode 100644 src/libraries/System.Speech/src/Result/SemanticValue.cs create mode 100644 src/libraries/System.Speech/src/SR.cs create mode 100644 src/libraries/System.Speech/src/SRID.cs create mode 100644 src/libraries/System.Speech/src/Synthesis/BookmarkEventArgs.cs create mode 100644 src/libraries/System.Speech/src/Synthesis/FilePrompt.cs create mode 100644 src/libraries/System.Speech/src/Synthesis/InstalledVoice.cs create mode 100644 src/libraries/System.Speech/src/Synthesis/PhonemeEventArgs.cs create mode 100644 src/libraries/System.Speech/src/Synthesis/Prompt.cs create mode 100644 src/libraries/System.Speech/src/Synthesis/PromptBuilder.cs create mode 100644 src/libraries/System.Speech/src/Synthesis/PromptEventArgs.cs create mode 100644 src/libraries/System.Speech/src/Synthesis/PromptStyle.cs create mode 100644 src/libraries/System.Speech/src/Synthesis/SpeakCompletedEventArgs.cs create mode 100644 src/libraries/System.Speech/src/Synthesis/SpeakProgressEventArgs.cs create mode 100644 src/libraries/System.Speech/src/Synthesis/SpeechSynthesizer.cs create mode 100644 src/libraries/System.Speech/src/Synthesis/SynthesizerStateChangedEventArgs.cs create mode 100644 src/libraries/System.Speech/src/Synthesis/TTSEngine/SAPIEngineTypes.cs create mode 100644 src/libraries/System.Speech/src/Synthesis/TTSEngine/TTSEngineTypes.cs create mode 100644 src/libraries/System.Speech/src/Synthesis/VisemeEventArgs.cs create mode 100644 src/libraries/System.Speech/src/Synthesis/VoiceChangeEventArgs.cs create mode 100644 src/libraries/System.Speech/src/Synthesis/VoiceInfo.cs create mode 100644 src/libraries/System.Speech/src/System.Speech.csproj create mode 100644 src/libraries/System.Speech/src/upstable_chs.upsmap create mode 100644 src/libraries/System.Speech/src/upstable_cht.upsmap create mode 100644 src/libraries/System.Speech/src/upstable_deu.upsmap create mode 100644 src/libraries/System.Speech/src/upstable_enu.upsmap create mode 100644 src/libraries/System.Speech/src/upstable_esp.upsmap create mode 100644 src/libraries/System.Speech/src/upstable_fra.upsmap create mode 100644 src/libraries/System.Speech/src/upstable_jpn.upsmap create mode 100644 src/libraries/System.Speech/tests/GrammarTests.cs create mode 100644 src/libraries/System.Speech/tests/OtherTests.cs create mode 100644 src/libraries/System.Speech/tests/SpeechRecognizerTests.cs create mode 100644 src/libraries/System.Speech/tests/SynthesizeRecognizeTests.cs create mode 100644 src/libraries/System.Speech/tests/System.Speech.Tests.csproj diff --git a/src/libraries/Common/src/System/SR.cs b/src/libraries/Common/src/System/SR.cs index 4eed64c2bf6449..20431f256a1acf 100644 --- a/src/libraries/Common/src/System/SR.cs +++ b/src/libraries/Common/src/System/SR.cs @@ -3,7 +3,6 @@ #nullable enable using System.Resources; -using System.Runtime.CompilerServices; namespace System { diff --git a/src/libraries/System.Private.CoreLib/src/System/Runtime/Versioning/PlatformAttributes.cs b/src/libraries/System.Private.CoreLib/src/System/Runtime/Versioning/PlatformAttributes.cs index 66564c1c26c42d..467a91165f2b1e 100644 --- a/src/libraries/System.Private.CoreLib/src/System/Runtime/Versioning/PlatformAttributes.cs +++ b/src/libraries/System.Private.CoreLib/src/System/Runtime/Versioning/PlatformAttributes.cs @@ -66,9 +66,9 @@ public TargetPlatformAttribute(string platformName) : base(platformName) #else internal #endif - sealed class SupportedOSPlatformAttribute : OSPlatformAttribute + sealed class SupportedOSPlatformAttribute : OSPlatformAttribute { - public SupportedOSPlatformAttribute (string platformName) : base(platformName) + public SupportedOSPlatformAttribute(string platformName) : base(platformName) { } } diff --git a/src/libraries/System.Speech/Directory.Build.props b/src/libraries/System.Speech/Directory.Build.props new file mode 100644 index 00000000000000..41646b3951d61d --- /dev/null +++ b/src/libraries/System.Speech/Directory.Build.props @@ -0,0 +1,16 @@ + + + + + 4.0.0.0 + Microsoft + true + Provides types to perform speech synthesis and speech recognition. + +Commonly Used Types +System.Speech.Synthesis.SpeechSynthesizer +System.Speech.Recognition.SpeechRecognizer + + \ No newline at end of file diff --git a/src/libraries/System.Speech/System.Speech.sln b/src/libraries/System.Speech/System.Speech.sln new file mode 100644 index 00000000000000..7da94f631eee09 --- /dev/null +++ b/src/libraries/System.Speech/System.Speech.sln @@ -0,0 +1,31 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 16 +VisualStudioVersion = 16.0.30709.18 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "System.Speech", "src\System.Speech.csproj", "{D6377882-BD90-46D6-AC60-83498E4BA2B3}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "System.Speech.Tests", "tests\System.Speech.Tests.csproj", "{E26B6065-4016-4385-9AB2-EEBE2C97CEE7}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {D6377882-BD90-46D6-AC60-83498E4BA2B3}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {D6377882-BD90-46D6-AC60-83498E4BA2B3}.Debug|Any CPU.Build.0 = Debug|Any CPU + {D6377882-BD90-46D6-AC60-83498E4BA2B3}.Release|Any CPU.ActiveCfg = Release|Any CPU + {D6377882-BD90-46D6-AC60-83498E4BA2B3}.Release|Any CPU.Build.0 = Release|Any CPU + {E26B6065-4016-4385-9AB2-EEBE2C97CEE7}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {E26B6065-4016-4385-9AB2-EEBE2C97CEE7}.Debug|Any CPU.Build.0 = Debug|Any CPU + {E26B6065-4016-4385-9AB2-EEBE2C97CEE7}.Release|Any CPU.ActiveCfg = Release|Any CPU + {E26B6065-4016-4385-9AB2-EEBE2C97CEE7}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {5BD9DF41-48EC-4FB8-998D-122857C5CA73} + EndGlobalSection +EndGlobal diff --git a/src/libraries/System.Speech/pkg/System.Speech.pkgproj b/src/libraries/System.Speech/pkg/System.Speech.pkgproj new file mode 100644 index 00000000000000..327c69d3d98c81 --- /dev/null +++ b/src/libraries/System.Speech/pkg/System.Speech.pkgproj @@ -0,0 +1,16 @@ + + + + + netcoreapp2.0;net45;uap10.0.16299;$(AllXamarinFrameworks) + + + + true + + + build\netcoreapp2.0\ + + + + diff --git a/src/libraries/System.Speech/pkg/build/System.Speech.targets b/src/libraries/System.Speech/pkg/build/System.Speech.targets new file mode 100644 index 00000000000000..840076be032c83 --- /dev/null +++ b/src/libraries/System.Speech/pkg/build/System.Speech.targets @@ -0,0 +1,7 @@ + + + + + diff --git a/src/libraries/System.Speech/ref/System.Speech.cs b/src/libraries/System.Speech/ref/System.Speech.cs new file mode 100644 index 00000000000000..65fa54c3647476 --- /dev/null +++ b/src/libraries/System.Speech/ref/System.Speech.cs @@ -0,0 +1,1157 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +//------------------------------------------------------------------------------ +// +// This code was generated by a tool. +// GenAPI Version: 6.0.3.5205 +// +// Changes to this file may cause incorrect behavior and will be lost if +// the code is regenerated. +// +//------------------------------------------------------------------------------ +namespace System.Speech.AudioFormat +{ + public enum AudioBitsPerSample + { + Eight = 8, + Sixteen = 16, + } + public enum AudioChannel + { + Mono = 1, + Stereo = 2, + } + public enum EncodingFormat + { + Pcm = 1, + ALaw = 6, + ULaw = 7, + } + public partial class SpeechAudioFormatInfo + { + public SpeechAudioFormatInfo(int samplesPerSecond, System.Speech.AudioFormat.AudioBitsPerSample bitsPerSample, System.Speech.AudioFormat.AudioChannel channel) { } + [System.ComponentModel.EditorBrowsableAttribute(System.ComponentModel.EditorBrowsableState.Never)] + public SpeechAudioFormatInfo(System.Speech.AudioFormat.EncodingFormat encodingFormat, int samplesPerSecond, int bitsPerSample, int channelCount, int averageBytesPerSecond, int blockAlign, byte[] formatSpecificData) { } + [System.ComponentModel.EditorBrowsableAttribute(System.ComponentModel.EditorBrowsableState.Never)] + public int AverageBytesPerSecond { get { throw null; } } + [System.ComponentModel.EditorBrowsableAttribute(System.ComponentModel.EditorBrowsableState.Never)] + public int BitsPerSample { get { throw null; } } + [System.ComponentModel.EditorBrowsableAttribute(System.ComponentModel.EditorBrowsableState.Never)] + public int BlockAlign { get { throw null; } } + public int ChannelCount { get { throw null; } } + public System.Speech.AudioFormat.EncodingFormat EncodingFormat { get { throw null; } } + public int SamplesPerSecond { get { throw null; } } + public override bool Equals(object obj) { throw null; } + public byte[] FormatSpecificData() { throw null; } + public override int GetHashCode() { throw null; } + } +} +namespace System.Speech.Recognition +{ + public partial class AudioLevelUpdatedEventArgs : System.EventArgs + { + internal AudioLevelUpdatedEventArgs() { } + public int AudioLevel { get { throw null; } } + } + public enum AudioSignalProblem + { + None = 0, + TooNoisy = 1, + NoSignal = 2, + TooLoud = 3, + TooSoft = 4, + TooFast = 5, + TooSlow = 6, + } + public partial class AudioSignalProblemOccurredEventArgs : System.EventArgs + { + internal AudioSignalProblemOccurredEventArgs() { } + public int AudioLevel { get { throw null; } } + public System.TimeSpan AudioPosition { get { throw null; } } + public System.Speech.Recognition.AudioSignalProblem AudioSignalProblem { get { throw null; } } + public System.TimeSpan RecognizerAudioPosition { get { throw null; } } + } + public enum AudioState + { + Stopped = 0, + Silence = 1, + Speech = 2, + } + public partial class AudioStateChangedEventArgs : System.EventArgs + { + internal AudioStateChangedEventArgs() { } + public System.Speech.Recognition.AudioState AudioState { get { throw null; } } + } + public partial class Choices + { + public Choices() { } + public Choices(params System.Speech.Recognition.GrammarBuilder[] alternateChoices) { } + public Choices(params string[] phrases) { } + public void Add(params System.Speech.Recognition.GrammarBuilder[] alternateChoices) { } + public void Add(params string[] phrases) { } + public System.Speech.Recognition.GrammarBuilder ToGrammarBuilder() { throw null; } + } + public partial class DictationGrammar : System.Speech.Recognition.Grammar + { + public DictationGrammar() { } + public DictationGrammar(string topic) { } + public void SetDictationContext(string precedingText, string subsequentText) { } + } + [System.FlagsAttribute] + public enum DisplayAttributes + { + None = 0, + ZeroTrailingSpaces = 2, + OneTrailingSpace = 4, + TwoTrailingSpaces = 8, + ConsumeLeadingSpaces = 16, + } + public partial class EmulateRecognizeCompletedEventArgs : System.ComponentModel.AsyncCompletedEventArgs + { + internal EmulateRecognizeCompletedEventArgs() : base (default(System.Exception), default(bool), default(object)) { } + public System.Speech.Recognition.RecognitionResult Result { get { throw null; } } + } + public partial class Grammar + { + protected Grammar() { } + public Grammar(System.IO.Stream stream) { } + public Grammar(System.IO.Stream stream, string ruleName) { } + public Grammar(System.IO.Stream stream, string ruleName, object[] parameters) { } + [System.ComponentModel.EditorBrowsableAttribute(System.ComponentModel.EditorBrowsableState.Never)] + public Grammar(System.IO.Stream stream, string ruleName, System.Uri baseUri) { } + [System.ComponentModel.EditorBrowsableAttribute(System.ComponentModel.EditorBrowsableState.Never)] + public Grammar(System.IO.Stream stream, string ruleName, System.Uri baseUri, object[] parameters) { } + public Grammar(System.Speech.Recognition.GrammarBuilder builder) { } + public Grammar(System.Speech.Recognition.SrgsGrammar.SrgsDocument srgsDocument) { } + public Grammar(System.Speech.Recognition.SrgsGrammar.SrgsDocument srgsDocument, string ruleName) { } + public Grammar(System.Speech.Recognition.SrgsGrammar.SrgsDocument srgsDocument, string ruleName, object[] parameters) { } + [System.ComponentModel.EditorBrowsableAttribute(System.ComponentModel.EditorBrowsableState.Never)] + public Grammar(System.Speech.Recognition.SrgsGrammar.SrgsDocument srgsDocument, string ruleName, System.Uri baseUri) { } + [System.ComponentModel.EditorBrowsableAttribute(System.ComponentModel.EditorBrowsableState.Never)] + public Grammar(System.Speech.Recognition.SrgsGrammar.SrgsDocument srgsDocument, string ruleName, System.Uri baseUri, object[] parameters) { } + public Grammar(string path) { } + public Grammar(string path, string ruleName) { } + public Grammar(string path, string ruleName, object[] parameters) { } + public bool Enabled { get { throw null; } set { } } + protected internal virtual bool IsStg { get { throw null; } } + public bool Loaded { get { throw null; } } + public string Name { get { throw null; } set { } } + public int Priority { get { throw null; } set { } } + protected string ResourceName { get { throw null; } set { } } + public string RuleName { get { throw null; } } + public float Weight { get { throw null; } set { } } + public event System.EventHandler SpeechRecognized { add { } remove { } } + public static System.Speech.Recognition.Grammar LoadLocalizedGrammarFromType(System.Type type, params object[] onInitParameters) { throw null; } + protected void StgInit(object[] parameters) { } + } + public partial class GrammarBuilder + { + public GrammarBuilder() { } + public GrammarBuilder(System.Speech.Recognition.Choices alternateChoices) { } + public GrammarBuilder(System.Speech.Recognition.GrammarBuilder builder, int minRepeat, int maxRepeat) { } + public GrammarBuilder(System.Speech.Recognition.SemanticResultKey key) { } + public GrammarBuilder(System.Speech.Recognition.SemanticResultValue value) { } + public GrammarBuilder(string phrase) { } + public GrammarBuilder(string phrase, int minRepeat, int maxRepeat) { } + public GrammarBuilder(string phrase, System.Speech.Recognition.SubsetMatchingMode subsetMatchingCriteria) { } + public System.Globalization.CultureInfo Culture { get { throw null; } set { } } + public string DebugShowPhrases { get { throw null; } } + public static System.Speech.Recognition.GrammarBuilder Add(System.Speech.Recognition.Choices choices, System.Speech.Recognition.GrammarBuilder builder) { throw null; } + public static System.Speech.Recognition.GrammarBuilder Add(System.Speech.Recognition.GrammarBuilder builder, System.Speech.Recognition.Choices choices) { throw null; } + public static System.Speech.Recognition.GrammarBuilder Add(System.Speech.Recognition.GrammarBuilder builder1, System.Speech.Recognition.GrammarBuilder builder2) { throw null; } + public static System.Speech.Recognition.GrammarBuilder Add(System.Speech.Recognition.GrammarBuilder builder, string phrase) { throw null; } + public static System.Speech.Recognition.GrammarBuilder Add(string phrase, System.Speech.Recognition.GrammarBuilder builder) { throw null; } + public void Append(System.Speech.Recognition.Choices alternateChoices) { } + public void Append(System.Speech.Recognition.GrammarBuilder builder) { } + public void Append(System.Speech.Recognition.GrammarBuilder builder, int minRepeat, int maxRepeat) { } + public void Append(System.Speech.Recognition.SemanticResultKey key) { } + public void Append(System.Speech.Recognition.SemanticResultValue value) { } + public void Append(string phrase) { } + public void Append(string phrase, int minRepeat, int maxRepeat) { } + public void Append(string phrase, System.Speech.Recognition.SubsetMatchingMode subsetMatchingCriteria) { } + public void AppendDictation() { } + public void AppendDictation(string category) { } + public void AppendRuleReference(string path) { } + public void AppendRuleReference(string path, string rule) { } + public void AppendWildcard() { } + public static System.Speech.Recognition.GrammarBuilder operator +(System.Speech.Recognition.Choices choices, System.Speech.Recognition.GrammarBuilder builder) { throw null; } + public static System.Speech.Recognition.GrammarBuilder operator +(System.Speech.Recognition.GrammarBuilder builder, System.Speech.Recognition.Choices choices) { throw null; } + public static System.Speech.Recognition.GrammarBuilder operator +(System.Speech.Recognition.GrammarBuilder builder1, System.Speech.Recognition.GrammarBuilder builder2) { throw null; } + public static System.Speech.Recognition.GrammarBuilder operator +(System.Speech.Recognition.GrammarBuilder builder, string phrase) { throw null; } + public static System.Speech.Recognition.GrammarBuilder operator +(string phrase, System.Speech.Recognition.GrammarBuilder builder) { throw null; } + public static implicit operator System.Speech.Recognition.GrammarBuilder (System.Speech.Recognition.Choices choices) { throw null; } + public static implicit operator System.Speech.Recognition.GrammarBuilder (System.Speech.Recognition.SemanticResultKey semanticKey) { throw null; } + public static implicit operator System.Speech.Recognition.GrammarBuilder (System.Speech.Recognition.SemanticResultValue semanticValue) { throw null; } + public static implicit operator System.Speech.Recognition.GrammarBuilder (string phrase) { throw null; } + } + public partial class LoadGrammarCompletedEventArgs : System.ComponentModel.AsyncCompletedEventArgs + { + internal LoadGrammarCompletedEventArgs() : base (default(System.Exception), default(bool), default(object)) { } + public System.Speech.Recognition.Grammar Grammar { get { throw null; } } + } + public abstract partial class RecognitionEventArgs : System.EventArgs + { + internal RecognitionEventArgs() { } + public System.Speech.Recognition.RecognitionResult Result { get { throw null; } } + } + public sealed partial class RecognitionResult : System.Speech.Recognition.RecognizedPhrase, System.Runtime.Serialization.ISerializable + { + internal RecognitionResult() { } + public System.Collections.ObjectModel.ReadOnlyCollection Alternates { get { throw null; } } + public System.Speech.Recognition.RecognizedAudio Audio { get { throw null; } } + public System.Speech.Recognition.RecognizedAudio GetAudioForWordRange(System.Speech.Recognition.RecognizedWordUnit firstWord, System.Speech.Recognition.RecognizedWordUnit lastWord) { throw null; } + void System.Runtime.Serialization.ISerializable.GetObjectData(System.Runtime.Serialization.SerializationInfo info, System.Runtime.Serialization.StreamingContext context) { } + } + public partial class RecognizeCompletedEventArgs : System.ComponentModel.AsyncCompletedEventArgs + { + internal RecognizeCompletedEventArgs() : base (default(System.Exception), default(bool), default(object)) { } + public System.TimeSpan AudioPosition { get { throw null; } } + public bool BabbleTimeout { get { throw null; } } + public bool InitialSilenceTimeout { get { throw null; } } + public bool InputStreamEnded { get { throw null; } } + public System.Speech.Recognition.RecognitionResult Result { get { throw null; } } + } + public partial class RecognizedAudio + { + internal RecognizedAudio() { } + public System.TimeSpan AudioPosition { get { throw null; } } + public System.TimeSpan Duration { get { throw null; } } + public System.Speech.AudioFormat.SpeechAudioFormatInfo Format { get { throw null; } } + public System.DateTime StartTime { get { throw null; } } + public System.Speech.Recognition.RecognizedAudio GetRange(System.TimeSpan audioPosition, System.TimeSpan duration) { throw null; } + public void WriteToAudioStream(System.IO.Stream outputStream) { } + public void WriteToWaveStream(System.IO.Stream outputStream) { } + } + public partial class RecognizedPhrase + { + internal RecognizedPhrase() { } + public float Confidence { get { throw null; } } + public System.Speech.Recognition.Grammar Grammar { get { throw null; } } + public int HomophoneGroupId { get { throw null; } } + public System.Collections.ObjectModel.ReadOnlyCollection Homophones { get { throw null; } } + public System.Collections.ObjectModel.Collection ReplacementWordUnits { get { throw null; } } + public System.Speech.Recognition.SemanticValue Semantics { get { throw null; } } + public string Text { get { throw null; } } + public System.Collections.ObjectModel.ReadOnlyCollection Words { get { throw null; } } + public System.Xml.XPath.IXPathNavigable ConstructSmlFromSemantics() { throw null; } + } + public partial class RecognizedWordUnit + { + public RecognizedWordUnit(string text, float confidence, string pronunciation, string lexicalForm, System.Speech.Recognition.DisplayAttributes displayAttributes, System.TimeSpan audioPosition, System.TimeSpan audioDuration) { } + public float Confidence { get { throw null; } } + public System.Speech.Recognition.DisplayAttributes DisplayAttributes { get { throw null; } } + public string LexicalForm { get { throw null; } } + public string Pronunciation { get { throw null; } } + public string Text { get { throw null; } } + } + public enum RecognizeMode + { + Single = 0, + Multiple = 1, + } + public partial class RecognizerInfo : System.IDisposable + { + internal RecognizerInfo() { } + public System.Collections.Generic.IDictionary AdditionalInfo { get { throw null; } } + public System.Globalization.CultureInfo Culture { get { throw null; } } + public string Description { get { throw null; } } + public string Id { get { throw null; } } + public string Name { get { throw null; } } + public System.Collections.ObjectModel.ReadOnlyCollection SupportedAudioFormats { get { throw null; } } + public void Dispose() { } + } + public enum RecognizerState + { + Stopped = 0, + Listening = 1, + } + public partial class RecognizerUpdateReachedEventArgs : System.EventArgs + { + internal RecognizerUpdateReachedEventArgs() { } + public System.TimeSpan AudioPosition { get { throw null; } } + public object UserToken { get { throw null; } } + } + [System.Runtime.InteropServices.StructLayoutAttribute(System.Runtime.InteropServices.LayoutKind.Sequential)] + public partial class ReplacementText + { + internal ReplacementText() { } + public int CountOfWords { get { throw null; } } + public System.Speech.Recognition.DisplayAttributes DisplayAttributes { get { throw null; } } + public int FirstWordIndex { get { throw null; } } + public string Text { get { throw null; } } + } + public partial class SemanticResultKey + { + public SemanticResultKey(string semanticResultKey, params System.Speech.Recognition.GrammarBuilder[] builders) { } + public SemanticResultKey(string semanticResultKey, params string[] phrases) { } + public System.Speech.Recognition.GrammarBuilder ToGrammarBuilder() { throw null; } + } + public partial class SemanticResultValue + { + public SemanticResultValue(object value) { } + public SemanticResultValue(System.Speech.Recognition.GrammarBuilder builder, object value) { } + public SemanticResultValue(string phrase, object value) { } + public System.Speech.Recognition.GrammarBuilder ToGrammarBuilder() { throw null; } + } + public sealed partial class SemanticValue : System.Collections.Generic.ICollection>, System.Collections.Generic.IDictionary, System.Collections.Generic.IEnumerable>, System.Collections.IEnumerable + { + public SemanticValue(object value) { } + public SemanticValue(string keyName, object value, float confidence) { } + public float Confidence { get { throw null; } } + public int Count { get { throw null; } } + public System.Speech.Recognition.SemanticValue this[string key] { get { throw null; } set { } } + bool System.Collections.Generic.ICollection>.IsReadOnly { get { throw null; } } + System.Collections.Generic.ICollection System.Collections.Generic.IDictionary.Keys { get { throw null; } } + System.Collections.Generic.ICollection System.Collections.Generic.IDictionary.Values { get { throw null; } } + public object Value { get { throw null; } } + public bool Contains(System.Collections.Generic.KeyValuePair item) { throw null; } + public bool ContainsKey(string key) { throw null; } + public override bool Equals(object obj) { throw null; } + public override int GetHashCode() { throw null; } + void System.Collections.Generic.ICollection>.Add(System.Collections.Generic.KeyValuePair key) { } + void System.Collections.Generic.ICollection>.Clear() { } + void System.Collections.Generic.ICollection>.CopyTo(System.Collections.Generic.KeyValuePair[] array, int index) { } + bool System.Collections.Generic.ICollection>.Remove(System.Collections.Generic.KeyValuePair key) { throw null; } + void System.Collections.Generic.IDictionary.Add(string key, System.Speech.Recognition.SemanticValue value) { } + bool System.Collections.Generic.IDictionary.Remove(string key) { throw null; } + bool System.Collections.Generic.IDictionary.TryGetValue(string key, out System.Speech.Recognition.SemanticValue value) { throw null; } + System.Collections.Generic.IEnumerator> System.Collections.Generic.IEnumerable>.GetEnumerator() { throw null; } + System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator() { throw null; } + } + public partial class SpeechDetectedEventArgs : System.EventArgs + { + internal SpeechDetectedEventArgs() { } + public System.TimeSpan AudioPosition { get { throw null; } } + } + public partial class SpeechHypothesizedEventArgs : System.Speech.Recognition.RecognitionEventArgs + { + internal SpeechHypothesizedEventArgs() { } + } + public partial class SpeechRecognitionEngine : System.IDisposable + { + public SpeechRecognitionEngine() { } + public SpeechRecognitionEngine(System.Globalization.CultureInfo culture) { } + public SpeechRecognitionEngine(System.Speech.Recognition.RecognizerInfo recognizerInfo) { } + public SpeechRecognitionEngine(string recognizerId) { } + public System.Speech.AudioFormat.SpeechAudioFormatInfo AudioFormat { get { throw null; } } + public int AudioLevel { get { throw null; } } + public System.TimeSpan AudioPosition { get { throw null; } } + public System.Speech.Recognition.AudioState AudioState { get { throw null; } } + [System.ComponentModel.EditorBrowsableAttribute(System.ComponentModel.EditorBrowsableState.Never)] + public System.TimeSpan BabbleTimeout { get { throw null; } set { } } + [System.ComponentModel.EditorBrowsableAttribute(System.ComponentModel.EditorBrowsableState.Never)] + public System.TimeSpan EndSilenceTimeout { get { throw null; } set { } } + [System.ComponentModel.EditorBrowsableAttribute(System.ComponentModel.EditorBrowsableState.Never)] + public System.TimeSpan EndSilenceTimeoutAmbiguous { get { throw null; } set { } } + public System.Collections.ObjectModel.ReadOnlyCollection Grammars { get { throw null; } } + [System.ComponentModel.EditorBrowsableAttribute(System.ComponentModel.EditorBrowsableState.Never)] + public System.TimeSpan InitialSilenceTimeout { get { throw null; } set { } } + public int MaxAlternates { get { throw null; } set { } } + public System.TimeSpan RecognizerAudioPosition { get { throw null; } } + public System.Speech.Recognition.RecognizerInfo RecognizerInfo { get { throw null; } } + public event System.EventHandler AudioLevelUpdated { add { } remove { } } + public event System.EventHandler AudioSignalProblemOccurred { add { } remove { } } + public event System.EventHandler AudioStateChanged { add { } remove { } } + public event System.EventHandler EmulateRecognizeCompleted { add { } remove { } } + public event System.EventHandler LoadGrammarCompleted { add { } remove { } } + public event System.EventHandler RecognizeCompleted { add { } remove { } } + public event System.EventHandler RecognizerUpdateReached { add { } remove { } } + public event System.EventHandler SpeechDetected { add { } remove { } } + public event System.EventHandler SpeechHypothesized { add { } remove { } } + public event System.EventHandler SpeechRecognitionRejected { add { } remove { } } + public event System.EventHandler SpeechRecognized { add { } remove { } } + public void Dispose() { } + protected virtual void Dispose(bool disposing) { } + public System.Speech.Recognition.RecognitionResult EmulateRecognize(System.Speech.Recognition.RecognizedWordUnit[] wordUnits, System.Globalization.CompareOptions compareOptions) { throw null; } + public System.Speech.Recognition.RecognitionResult EmulateRecognize(string inputText) { throw null; } + public System.Speech.Recognition.RecognitionResult EmulateRecognize(string inputText, System.Globalization.CompareOptions compareOptions) { throw null; } + public void EmulateRecognizeAsync(System.Speech.Recognition.RecognizedWordUnit[] wordUnits, System.Globalization.CompareOptions compareOptions) { } + public void EmulateRecognizeAsync(string inputText) { } + public void EmulateRecognizeAsync(string inputText, System.Globalization.CompareOptions compareOptions) { } + public static System.Collections.ObjectModel.ReadOnlyCollection InstalledRecognizers() { throw null; } + public void LoadGrammar(System.Speech.Recognition.Grammar grammar) { } + public void LoadGrammarAsync(System.Speech.Recognition.Grammar grammar) { } + public object QueryRecognizerSetting(string settingName) { throw null; } + public System.Speech.Recognition.RecognitionResult Recognize() { throw null; } + public System.Speech.Recognition.RecognitionResult Recognize(System.TimeSpan initialSilenceTimeout) { throw null; } + public void RecognizeAsync() { } + public void RecognizeAsync(System.Speech.Recognition.RecognizeMode mode) { } + public void RecognizeAsyncCancel() { } + public void RecognizeAsyncStop() { } + public void RequestRecognizerUpdate() { } + public void RequestRecognizerUpdate(object userToken) { } + public void RequestRecognizerUpdate(object userToken, System.TimeSpan audioPositionAheadToRaiseUpdate) { } + public void SetInputToAudioStream(System.IO.Stream audioSource, System.Speech.AudioFormat.SpeechAudioFormatInfo audioFormat) { } + public void SetInputToDefaultAudioDevice() { } + public void SetInputToNull() { } + public void SetInputToWaveFile(string path) { } + public void SetInputToWaveStream(System.IO.Stream audioSource) { } + public void UnloadAllGrammars() { } + public void UnloadGrammar(System.Speech.Recognition.Grammar grammar) { } + public void UpdateRecognizerSetting(string settingName, int updatedValue) { } + public void UpdateRecognizerSetting(string settingName, string updatedValue) { } + } + public partial class SpeechRecognitionRejectedEventArgs : System.Speech.Recognition.RecognitionEventArgs + { + internal SpeechRecognitionRejectedEventArgs() { } + } + public partial class SpeechRecognizedEventArgs : System.Speech.Recognition.RecognitionEventArgs + { + internal SpeechRecognizedEventArgs() { } + } + public partial class SpeechRecognizer : System.IDisposable + { + public SpeechRecognizer() { } + public System.Speech.AudioFormat.SpeechAudioFormatInfo AudioFormat { get { throw null; } } + public int AudioLevel { get { throw null; } } + public System.TimeSpan AudioPosition { get { throw null; } } + public System.Speech.Recognition.AudioState AudioState { get { throw null; } } + public bool Enabled { get { throw null; } set { } } + public System.Collections.ObjectModel.ReadOnlyCollection Grammars { get { throw null; } } + public int MaxAlternates { get { throw null; } set { } } + public bool PauseRecognizerOnRecognition { get { throw null; } set { } } + public System.TimeSpan RecognizerAudioPosition { get { throw null; } } + public System.Speech.Recognition.RecognizerInfo RecognizerInfo { get { throw null; } } + public System.Speech.Recognition.RecognizerState State { get { throw null; } } + public event System.EventHandler AudioLevelUpdated { add { } remove { } } + public event System.EventHandler AudioSignalProblemOccurred { add { } remove { } } + public event System.EventHandler AudioStateChanged { add { } remove { } } + public event System.EventHandler EmulateRecognizeCompleted { add { } remove { } } + public event System.EventHandler LoadGrammarCompleted { add { } remove { } } + public event System.EventHandler RecognizerUpdateReached { add { } remove { } } + public event System.EventHandler SpeechDetected { add { } remove { } } + public event System.EventHandler SpeechHypothesized { add { } remove { } } + public event System.EventHandler SpeechRecognitionRejected { add { } remove { } } + public event System.EventHandler SpeechRecognized { add { } remove { } } + public event System.EventHandler StateChanged { add { } remove { } } + public void Dispose() { } + protected virtual void Dispose(bool disposing) { } + public System.Speech.Recognition.RecognitionResult EmulateRecognize(System.Speech.Recognition.RecognizedWordUnit[] wordUnits, System.Globalization.CompareOptions compareOptions) { throw null; } + public System.Speech.Recognition.RecognitionResult EmulateRecognize(string inputText) { throw null; } + public System.Speech.Recognition.RecognitionResult EmulateRecognize(string inputText, System.Globalization.CompareOptions compareOptions) { throw null; } + public void EmulateRecognizeAsync(System.Speech.Recognition.RecognizedWordUnit[] wordUnits, System.Globalization.CompareOptions compareOptions) { } + public void EmulateRecognizeAsync(string inputText) { } + public void EmulateRecognizeAsync(string inputText, System.Globalization.CompareOptions compareOptions) { } + public void LoadGrammar(System.Speech.Recognition.Grammar grammar) { } + public void LoadGrammarAsync(System.Speech.Recognition.Grammar grammar) { } + public void RequestRecognizerUpdate() { } + public void RequestRecognizerUpdate(object userToken) { } + public void RequestRecognizerUpdate(object userToken, System.TimeSpan audioPositionAheadToRaiseUpdate) { } + public void UnloadAllGrammars() { } + public void UnloadGrammar(System.Speech.Recognition.Grammar grammar) { } + } + public partial class SpeechUI + { + internal SpeechUI() { } + public static bool SendTextFeedback(System.Speech.Recognition.RecognitionResult result, string feedback, bool isSuccessfulAction) { throw null; } + } + public partial class StateChangedEventArgs : System.EventArgs + { + internal StateChangedEventArgs() { } + public System.Speech.Recognition.RecognizerState RecognizerState { get { throw null; } } + } + public enum SubsetMatchingMode + { + Subsequence = 0, + OrderedSubset = 1, + SubsequenceContentRequired = 2, + OrderedSubsetContentRequired = 3, + } +} +namespace System.Speech.Recognition.SrgsGrammar +{ + public partial class SrgsDocument + { + public SrgsDocument() { } + public SrgsDocument(System.Speech.Recognition.GrammarBuilder builder) { } + public SrgsDocument(System.Speech.Recognition.SrgsGrammar.SrgsRule grammarRootRule) { } + public SrgsDocument(string path) { } + public SrgsDocument(System.Xml.XmlReader srgsGrammar) { } + public System.Collections.ObjectModel.Collection AssemblyReferences { get { throw null; } } + public System.Collections.ObjectModel.Collection CodeBehind { get { throw null; } } + public System.Globalization.CultureInfo Culture { get { throw null; } set { } } + public bool Debug { get { throw null; } set { } } + public System.Collections.ObjectModel.Collection ImportNamespaces { get { throw null; } } + public string Language { get { throw null; } set { } } + public System.Speech.Recognition.SrgsGrammar.SrgsGrammarMode Mode { get { throw null; } set { } } + public string Namespace { get { throw null; } set { } } + public System.Speech.Recognition.SrgsGrammar.SrgsPhoneticAlphabet PhoneticAlphabet { get { throw null; } set { } } + public System.Speech.Recognition.SrgsGrammar.SrgsRule Root { get { throw null; } set { } } + public System.Speech.Recognition.SrgsGrammar.SrgsRulesCollection Rules { get { throw null; } } + public string Script { get { throw null; } set { } } + public System.Uri XmlBase { get { throw null; } set { } } + public void WriteSrgs(System.Xml.XmlWriter srgsGrammar) { } + } + public abstract partial class SrgsElement : System.MarshalByRefObject + { + protected SrgsElement() { } + internal abstract string DebuggerDisplayString(); + internal abstract void WriteSrgs(System.Xml.XmlWriter writer); + } + public static partial class SrgsGrammarCompiler + { + public static void Compile(System.Speech.Recognition.SrgsGrammar.SrgsDocument srgsGrammar, System.IO.Stream outputStream) { } + public static void Compile(string inputPath, System.IO.Stream outputStream) { } + public static void Compile(System.Xml.XmlReader reader, System.IO.Stream outputStream) { } + public static void CompileClassLibrary(System.Speech.Recognition.SrgsGrammar.SrgsDocument srgsGrammar, string outputPath, string[] referencedAssemblies, string keyFile) { } + public static void CompileClassLibrary(string[] inputPaths, string outputPath, string[] referencedAssemblies, string keyFile) { } + public static void CompileClassLibrary(System.Xml.XmlReader reader, string outputPath, string[] referencedAssemblies, string keyFile) { } + } + public enum SrgsGrammarMode + { + Voice = 0, + Dtmf = 1, + } + public partial class SrgsItem : System.Speech.Recognition.SrgsGrammar.SrgsElement + { + public SrgsItem() { } + public SrgsItem(int repeatCount) { } + public SrgsItem(int min, int max) { } + public SrgsItem(int min, int max, params System.Speech.Recognition.SrgsGrammar.SrgsElement[] elements) { } + public SrgsItem(int min, int max, string text) { } + public SrgsItem(params System.Speech.Recognition.SrgsGrammar.SrgsElement[] elements) { } + public SrgsItem(string text) { } + public System.Collections.ObjectModel.Collection Elements { get { throw null; } } + public int MaxRepeat { get { throw null; } } + public int MinRepeat { get { throw null; } } + public float RepeatProbability { get { throw null; } set { } } + public float Weight { get { throw null; } set { } } + public void Add(System.Speech.Recognition.SrgsGrammar.SrgsElement element) { } + public void SetRepeat(int count) { } + public void SetRepeat(int minRepeat, int maxRepeat) { } + internal override string DebuggerDisplayString() { throw null; } + internal override void WriteSrgs(System.Xml.XmlWriter writer) { throw null; } + } + public partial class SrgsNameValueTag : System.Speech.Recognition.SrgsGrammar.SrgsElement + { + public SrgsNameValueTag() { } + public SrgsNameValueTag(object value) { } + public SrgsNameValueTag(string name, object value) { } + public string Name { get { throw null; } set { } } + public object Value { get { throw null; } set { } } + internal override string DebuggerDisplayString() { throw null; } + internal override void WriteSrgs(System.Xml.XmlWriter writer) { throw null; } + } + public partial class SrgsOneOf : System.Speech.Recognition.SrgsGrammar.SrgsElement + { + public SrgsOneOf() { } + public SrgsOneOf(params System.Speech.Recognition.SrgsGrammar.SrgsItem[] items) { } + public SrgsOneOf(params string[] items) { } + public System.Collections.ObjectModel.Collection Items { get { throw null; } } + public void Add(System.Speech.Recognition.SrgsGrammar.SrgsItem item) { } + internal override string DebuggerDisplayString() { throw null; } + internal override void WriteSrgs(System.Xml.XmlWriter writer) { throw null; } + } + public enum SrgsPhoneticAlphabet + { + Sapi = 0, + Ipa = 1, + Ups = 2, + } + public partial class SrgsRule + { + public SrgsRule(string id) { } + public SrgsRule(string id, params System.Speech.Recognition.SrgsGrammar.SrgsElement[] elements) { } + public string BaseClass { get { throw null; } set { } } + public System.Collections.ObjectModel.Collection Elements { get { throw null; } } + public string Id { get { throw null; } set { } } + public string OnError { get { throw null; } set { } } + public string OnInit { get { throw null; } set { } } + public string OnParse { get { throw null; } set { } } + public string OnRecognition { get { throw null; } set { } } + public System.Speech.Recognition.SrgsGrammar.SrgsRuleScope Scope { get { throw null; } set { } } + public string Script { get { throw null; } set { } } + public void Add(System.Speech.Recognition.SrgsGrammar.SrgsElement element) { } + } + [System.ComponentModel.ImmutableObjectAttribute(true)] + public partial class SrgsRuleRef : System.Speech.Recognition.SrgsGrammar.SrgsElement + { + public static readonly System.Speech.Recognition.SrgsGrammar.SrgsRuleRef Dictation; + public static readonly System.Speech.Recognition.SrgsGrammar.SrgsRuleRef Garbage; + public static readonly System.Speech.Recognition.SrgsGrammar.SrgsRuleRef MnemonicSpelling; + public static readonly System.Speech.Recognition.SrgsGrammar.SrgsRuleRef Null; + public static readonly System.Speech.Recognition.SrgsGrammar.SrgsRuleRef Void; + public SrgsRuleRef(System.Speech.Recognition.SrgsGrammar.SrgsRule rule) { } + public SrgsRuleRef(System.Speech.Recognition.SrgsGrammar.SrgsRule rule, string semanticKey) { } + public SrgsRuleRef(System.Speech.Recognition.SrgsGrammar.SrgsRule rule, string semanticKey, string parameters) { } + public SrgsRuleRef(System.Uri uri) { } + public SrgsRuleRef(System.Uri uri, string rule) { } + public SrgsRuleRef(System.Uri uri, string rule, string semanticKey) { } + public SrgsRuleRef(System.Uri uri, string rule, string semanticKey, string parameters) { } + public string Params { get { throw null; } } + public string SemanticKey { get { throw null; } } + public System.Uri Uri { get { throw null; } } + internal override string DebuggerDisplayString() { throw null; } + internal override void WriteSrgs(System.Xml.XmlWriter writer) { throw null; } + } + public sealed partial class SrgsRulesCollection : System.Collections.ObjectModel.KeyedCollection + { + public SrgsRulesCollection() { } + public void Add(params System.Speech.Recognition.SrgsGrammar.SrgsRule[] rules) { } + protected override string GetKeyForItem(System.Speech.Recognition.SrgsGrammar.SrgsRule rule) { throw null; } + } + public enum SrgsRuleScope + { + Public = 0, + Private = 1, + } + public partial class SrgsSemanticInterpretationTag : System.Speech.Recognition.SrgsGrammar.SrgsElement + { + public SrgsSemanticInterpretationTag() { } + public SrgsSemanticInterpretationTag(string script) { } + public string Script { get { throw null; } set { } } + internal override string DebuggerDisplayString() { throw null; } + internal override void WriteSrgs(System.Xml.XmlWriter writer) { throw null; } + } + public partial class SrgsSubset : System.Speech.Recognition.SrgsGrammar.SrgsElement + { + public SrgsSubset(string text) { } + public SrgsSubset(string text, System.Speech.Recognition.SubsetMatchingMode matchingMode) { } + public System.Speech.Recognition.SubsetMatchingMode MatchingMode { get { throw null; } set { } } + public string Text { get { throw null; } set { } } + internal override string DebuggerDisplayString() { throw null; } + internal override void WriteSrgs(System.Xml.XmlWriter writer) { throw null; } + } + public partial class SrgsText : System.Speech.Recognition.SrgsGrammar.SrgsElement + { + public SrgsText() { } + public SrgsText(string text) { } + public string Text { get { throw null; } set { } } + internal override string DebuggerDisplayString() { throw null; } + internal override void WriteSrgs(System.Xml.XmlWriter writer) { throw null; } + } + public partial class SrgsToken : System.Speech.Recognition.SrgsGrammar.SrgsElement + { + public SrgsToken(string text) { } + public string Display { get { throw null; } set { } } + public string Pronunciation { get { throw null; } set { } } + public string Text { get { throw null; } set { } } + internal override string DebuggerDisplayString() { throw null; } + internal override void WriteSrgs(System.Xml.XmlWriter writer) { throw null; } + } +} +namespace System.Speech.Synthesis +{ + public partial class BookmarkReachedEventArgs : System.Speech.Synthesis.PromptEventArgs + { + internal BookmarkReachedEventArgs() { } + public System.TimeSpan AudioPosition { get { throw null; } } + public string Bookmark { get { throw null; } } + } + public partial class FilePrompt : System.Speech.Synthesis.Prompt + { + public FilePrompt(string path, System.Speech.Synthesis.SynthesisMediaType media) : base (default(string)) { } + public FilePrompt(System.Uri promptFile, System.Speech.Synthesis.SynthesisMediaType media) : base (default(string)) { } + } + public partial class InstalledVoice + { + internal InstalledVoice() { } + public bool Enabled { get { throw null; } set { } } + public System.Speech.Synthesis.VoiceInfo VoiceInfo { get { throw null; } } + public override bool Equals(object obj) { throw null; } + public override int GetHashCode() { throw null; } + } + public partial class PhonemeReachedEventArgs : System.Speech.Synthesis.PromptEventArgs + { + internal PhonemeReachedEventArgs() { } + public System.TimeSpan AudioPosition { get { throw null; } } + public System.TimeSpan Duration { get { throw null; } } + public System.Speech.Synthesis.SynthesizerEmphasis Emphasis { get { throw null; } } + public string NextPhoneme { get { throw null; } } + public string Phoneme { get { throw null; } } + } + public partial class Prompt + { + public Prompt(System.Speech.Synthesis.PromptBuilder promptBuilder) { } + public Prompt(string textToSpeak) { } + public Prompt(string textToSpeak, System.Speech.Synthesis.SynthesisTextFormat media) { } + public bool IsCompleted { get { throw null; } } + } + public enum PromptBreak + { + None = 0, + ExtraSmall = 1, + Small = 2, + Medium = 3, + Large = 4, + ExtraLarge = 5, + } + public partial class PromptBuilder + { + public PromptBuilder() { } + public PromptBuilder(System.Globalization.CultureInfo culture) { } + public System.Globalization.CultureInfo Culture { get { throw null; } set { } } + public bool IsEmpty { get { throw null; } } + public void AppendAudio(string path) { } + public void AppendAudio(System.Uri audioFile) { } + public void AppendAudio(System.Uri audioFile, string alternateText) { } + public void AppendBookmark(string bookmarkName) { } + public void AppendBreak() { } + public void AppendBreak(System.Speech.Synthesis.PromptBreak strength) { } + public void AppendBreak(System.TimeSpan duration) { } + public void AppendPromptBuilder(System.Speech.Synthesis.PromptBuilder promptBuilder) { } + public void AppendSsml(string path) { } + public void AppendSsml(System.Uri ssmlFile) { } + public void AppendSsml(System.Xml.XmlReader ssmlFile) { } + [System.ComponentModel.EditorBrowsableAttribute(System.ComponentModel.EditorBrowsableState.Never)] + public void AppendSsmlMarkup(string ssmlMarkup) { } + public void AppendText(string textToSpeak) { } + public void AppendText(string textToSpeak, System.Speech.Synthesis.PromptEmphasis emphasis) { } + public void AppendText(string textToSpeak, System.Speech.Synthesis.PromptRate rate) { } + public void AppendText(string textToSpeak, System.Speech.Synthesis.PromptVolume volume) { } + public void AppendTextWithAlias(string textToSpeak, string substitute) { } + public void AppendTextWithHint(string textToSpeak, System.Speech.Synthesis.SayAs sayAs) { } + public void AppendTextWithHint(string textToSpeak, string sayAs) { } + public void AppendTextWithPronunciation(string textToSpeak, string pronunciation) { } + public void ClearContent() { } + public void EndParagraph() { } + public void EndSentence() { } + public void EndStyle() { } + public void EndVoice() { } + public void StartParagraph() { } + public void StartParagraph(System.Globalization.CultureInfo culture) { } + public void StartSentence() { } + public void StartSentence(System.Globalization.CultureInfo culture) { } + public void StartStyle(System.Speech.Synthesis.PromptStyle style) { } + public void StartVoice(System.Globalization.CultureInfo culture) { } + public void StartVoice(System.Speech.Synthesis.VoiceGender gender) { } + public void StartVoice(System.Speech.Synthesis.VoiceGender gender, System.Speech.Synthesis.VoiceAge age) { } + public void StartVoice(System.Speech.Synthesis.VoiceGender gender, System.Speech.Synthesis.VoiceAge age, int voiceAlternate) { } + public void StartVoice(System.Speech.Synthesis.VoiceInfo voice) { } + public void StartVoice(string name) { } + public string ToXml() { throw null; } + } + public enum PromptEmphasis + { + NotSet = 0, + Strong = 1, + Moderate = 2, + None = 3, + Reduced = 4, + } + public abstract partial class PromptEventArgs : System.ComponentModel.AsyncCompletedEventArgs + { + internal PromptEventArgs() : base (default(System.Exception), default(bool), default(object)) { } + public System.Speech.Synthesis.Prompt Prompt { get { throw null; } } + } + public enum PromptRate + { + NotSet = 0, + ExtraFast = 1, + Fast = 2, + Medium = 3, + Slow = 4, + ExtraSlow = 5, + } + public partial class PromptStyle + { + public PromptStyle() { } + public PromptStyle(System.Speech.Synthesis.PromptEmphasis emphasis) { } + public PromptStyle(System.Speech.Synthesis.PromptRate rate) { } + public PromptStyle(System.Speech.Synthesis.PromptVolume volume) { } + public System.Speech.Synthesis.PromptEmphasis Emphasis { get { throw null; } set { } } + public System.Speech.Synthesis.PromptRate Rate { get { throw null; } set { } } + public System.Speech.Synthesis.PromptVolume Volume { get { throw null; } set { } } + } + public enum PromptVolume + { + NotSet = 0, + Silent = 1, + ExtraSoft = 2, + Soft = 3, + Medium = 4, + Loud = 5, + ExtraLoud = 6, + Default = 7, + } + public enum SayAs + { + SpellOut = 0, + NumberOrdinal = 1, + NumberCardinal = 2, + Date = 3, + DayMonthYear = 4, + MonthDayYear = 5, + YearMonthDay = 6, + YearMonth = 7, + MonthYear = 8, + MonthDay = 9, + DayMonth = 10, + Year = 11, + Month = 12, + Day = 13, + Time = 14, + Time24 = 15, + Time12 = 16, + Telephone = 17, + Text = 18, + } + public partial class SpeakCompletedEventArgs : System.Speech.Synthesis.PromptEventArgs + { + internal SpeakCompletedEventArgs() { } + } + public partial class SpeakProgressEventArgs : System.Speech.Synthesis.PromptEventArgs + { + internal SpeakProgressEventArgs() { } + public System.TimeSpan AudioPosition { get { throw null; } } + public int CharacterCount { get { throw null; } } + public int CharacterPosition { get { throw null; } } + public string Text { get { throw null; } } + } + public partial class SpeakStartedEventArgs : System.Speech.Synthesis.PromptEventArgs + { + internal SpeakStartedEventArgs() { } + } + public sealed partial class SpeechSynthesizer : System.IDisposable + { + public SpeechSynthesizer() { } + public int Rate { get { throw null; } set { } } + public System.Speech.Synthesis.SynthesizerState State { get { throw null; } } + public System.Speech.Synthesis.VoiceInfo Voice { get { throw null; } } + public int Volume { get { throw null; } set { } } + public event System.EventHandler BookmarkReached { add { } remove { } } + public event System.EventHandler PhonemeReached { add { } remove { } } + public event System.EventHandler SpeakCompleted { add { } remove { } } + public event System.EventHandler SpeakProgress { add { } remove { } } + public event System.EventHandler SpeakStarted { add { } remove { } } + public event System.EventHandler StateChanged { add { } remove { } } + public event System.EventHandler VisemeReached { add { } remove { } } + public event System.EventHandler VoiceChange { add { } remove { } } + public void AddLexicon(System.Uri uri, string mediaType) { } + public void Dispose() { } + ~SpeechSynthesizer() { } + public System.Speech.Synthesis.Prompt GetCurrentlySpokenPrompt() { throw null; } + public System.Collections.ObjectModel.ReadOnlyCollection GetInstalledVoices() { throw null; } + public System.Collections.ObjectModel.ReadOnlyCollection GetInstalledVoices(System.Globalization.CultureInfo culture) { throw null; } + public void Pause() { } + public void RemoveLexicon(System.Uri uri) { } + public void Resume() { } + public void SelectVoice(string name) { } + public void SelectVoiceByHints(System.Speech.Synthesis.VoiceGender gender) { } + public void SelectVoiceByHints(System.Speech.Synthesis.VoiceGender gender, System.Speech.Synthesis.VoiceAge age) { } + public void SelectVoiceByHints(System.Speech.Synthesis.VoiceGender gender, System.Speech.Synthesis.VoiceAge age, int voiceAlternate) { } + public void SelectVoiceByHints(System.Speech.Synthesis.VoiceGender gender, System.Speech.Synthesis.VoiceAge age, int voiceAlternate, System.Globalization.CultureInfo culture) { } + public void SetOutputToAudioStream(System.IO.Stream audioDestination, System.Speech.AudioFormat.SpeechAudioFormatInfo formatInfo) { } + public void SetOutputToDefaultAudioDevice() { } + public void SetOutputToNull() { } + public void SetOutputToWaveFile(string path) { } + public void SetOutputToWaveFile(string path, System.Speech.AudioFormat.SpeechAudioFormatInfo formatInfo) { } + public void SetOutputToWaveStream(System.IO.Stream audioDestination) { } + public void Speak(System.Speech.Synthesis.Prompt prompt) { } + public void Speak(System.Speech.Synthesis.PromptBuilder promptBuilder) { } + public void Speak(string textToSpeak) { } + public void SpeakAsync(System.Speech.Synthesis.Prompt prompt) { } + public System.Speech.Synthesis.Prompt SpeakAsync(System.Speech.Synthesis.PromptBuilder promptBuilder) { throw null; } + public System.Speech.Synthesis.Prompt SpeakAsync(string textToSpeak) { throw null; } + public void SpeakAsyncCancel(System.Speech.Synthesis.Prompt prompt) { } + public void SpeakAsyncCancelAll() { } + public void SpeakSsml(string textToSpeak) { } + public System.Speech.Synthesis.Prompt SpeakSsmlAsync(string textToSpeak) { throw null; } + } + public partial class StateChangedEventArgs : System.EventArgs + { + internal StateChangedEventArgs() { } + public System.Speech.Synthesis.SynthesizerState PreviousState { get { throw null; } } + public System.Speech.Synthesis.SynthesizerState State { get { throw null; } } + } + public enum SynthesisMediaType + { + Text = 0, + Ssml = 1, + WaveAudio = 2, + } + public enum SynthesisTextFormat + { + Text = 0, + Ssml = 1, + } + [System.FlagsAttribute] + public enum SynthesizerEmphasis + { + Stressed = 1, + Emphasized = 2, + } + public enum SynthesizerState + { + Ready = 0, + Speaking = 1, + Paused = 2, + } + public partial class VisemeReachedEventArgs : System.Speech.Synthesis.PromptEventArgs + { + internal VisemeReachedEventArgs() { } + public System.TimeSpan AudioPosition { get { throw null; } } + public System.TimeSpan Duration { get { throw null; } } + public System.Speech.Synthesis.SynthesizerEmphasis Emphasis { get { throw null; } } + public int NextViseme { get { throw null; } } + public int Viseme { get { throw null; } } + } + public enum VoiceAge + { + NotSet = 0, + Child = 10, + Teen = 15, + Adult = 30, + Senior = 65, + } + public partial class VoiceChangeEventArgs : System.Speech.Synthesis.PromptEventArgs + { + internal VoiceChangeEventArgs() { } + public System.Speech.Synthesis.VoiceInfo Voice { get { throw null; } } + } + public enum VoiceGender + { + NotSet = 0, + Male = 1, + Female = 2, + Neutral = 3, + } + public partial class VoiceInfo + { + internal VoiceInfo() { } + [System.ComponentModel.EditorBrowsableAttribute(System.ComponentModel.EditorBrowsableState.Never)] + public System.Collections.Generic.IDictionary AdditionalInfo { get { throw null; } } + public System.Speech.Synthesis.VoiceAge Age { get { throw null; } } + public System.Globalization.CultureInfo Culture { get { throw null; } } + public string Description { get { throw null; } } + public System.Speech.Synthesis.VoiceGender Gender { get { throw null; } } + public string Id { get { throw null; } } + public string Name { get { throw null; } } + [System.ComponentModel.EditorBrowsableAttribute(System.ComponentModel.EditorBrowsableState.Never)] + public System.Collections.ObjectModel.ReadOnlyCollection SupportedAudioFormats { get { throw null; } } + public override bool Equals(object obj) { throw null; } + public override int GetHashCode() { throw null; } + } +} +namespace System.Speech.Synthesis.TtsEngine +{ + [System.ComponentModel.ImmutableObjectAttribute(true)] + [System.Runtime.InteropServices.StructLayoutAttribute(System.Runtime.InteropServices.LayoutKind.Sequential)] + public partial struct ContourPoint : System.IEquatable + { + private object _dummy; + private int _dummyPrimitive; + public ContourPoint(float start, float change, System.Speech.Synthesis.TtsEngine.ContourPointChangeType changeType) { throw null; } + public float Change { get { throw null; } } + public System.Speech.Synthesis.TtsEngine.ContourPointChangeType ChangeType { get { throw null; } } + public float Start { get { throw null; } } + public override bool Equals(object obj) { throw null; } + public bool Equals(System.Speech.Synthesis.TtsEngine.ContourPoint other) { throw null; } + public override int GetHashCode() { throw null; } + public static bool operator ==(System.Speech.Synthesis.TtsEngine.ContourPoint point1, System.Speech.Synthesis.TtsEngine.ContourPoint point2) { throw null; } + public static bool operator !=(System.Speech.Synthesis.TtsEngine.ContourPoint point1, System.Speech.Synthesis.TtsEngine.ContourPoint point2) { throw null; } + } + public enum ContourPointChangeType + { + Hz = 0, + Percentage = 1, + } + public enum EmphasisBreak + { + Default = -7, + ExtraStrong = -6, + Strong = -5, + Medium = -4, + Weak = -3, + ExtraWeak = -2, + None = -1, + } + public enum EmphasisWord + { + Default = 0, + Strong = 1, + Moderate = 2, + None = 3, + Reduced = 4, + } + public enum EventParameterType + { + Undefined = 0, + Token = 1, + Object = 2, + Pointer = 3, + String = 4, + } + [System.ComponentModel.ImmutableObjectAttribute(true)] + [System.Runtime.InteropServices.StructLayoutAttribute(System.Runtime.InteropServices.LayoutKind.Sequential)] + public partial struct FragmentState : System.IEquatable + { + private object _dummy; + private int _dummyPrimitive; + public FragmentState(System.Speech.Synthesis.TtsEngine.TtsEngineAction action, int langId, int emphasis, int duration, System.Speech.Synthesis.TtsEngine.SayAs sayAs, System.Speech.Synthesis.TtsEngine.Prosody prosody, char[] phonemes) { throw null; } + public System.Speech.Synthesis.TtsEngine.TtsEngineAction Action { get { throw null; } } + public int Duration { get { throw null; } } + public int Emphasis { get { throw null; } } + public int LangId { get { throw null; } } + public char[] Phoneme { get { throw null; } } + public System.Speech.Synthesis.TtsEngine.Prosody Prosody { get { throw null; } } + public System.Speech.Synthesis.TtsEngine.SayAs SayAs { get { throw null; } } + public override bool Equals(object obj) { throw null; } + public bool Equals(System.Speech.Synthesis.TtsEngine.FragmentState other) { throw null; } + public override int GetHashCode() { throw null; } + public static bool operator ==(System.Speech.Synthesis.TtsEngine.FragmentState state1, System.Speech.Synthesis.TtsEngine.FragmentState state2) { throw null; } + public static bool operator !=(System.Speech.Synthesis.TtsEngine.FragmentState state1, System.Speech.Synthesis.TtsEngine.FragmentState state2) { throw null; } + } + public partial interface ITtsEngineSite + { + int Actions { get; } + int EventInterest { get; } + int Rate { get; } + int Volume { get; } + void AddEvents(System.Speech.Synthesis.TtsEngine.SpeechEventInfo[] events, int count); + void CompleteSkip(int skipped); + System.Speech.Synthesis.TtsEngine.SkipInfo GetSkipInfo(); + System.IO.Stream LoadResource(System.Uri uri, string mediaType); + int Write(System.IntPtr data, int count); + } + [System.Runtime.InteropServices.StructLayoutAttribute(System.Runtime.InteropServices.LayoutKind.Sequential)] + public partial class Prosody + { + public Prosody() { } + public int Duration { get { throw null; } set { } } + public System.Speech.Synthesis.TtsEngine.ProsodyNumber Pitch { get { throw null; } set { } } + public System.Speech.Synthesis.TtsEngine.ProsodyNumber Range { get { throw null; } set { } } + public System.Speech.Synthesis.TtsEngine.ProsodyNumber Rate { get { throw null; } set { } } + public System.Speech.Synthesis.TtsEngine.ProsodyNumber Volume { get { throw null; } set { } } + public System.Speech.Synthesis.TtsEngine.ContourPoint[] GetContourPoints() { throw null; } + public void SetContourPoints(System.Speech.Synthesis.TtsEngine.ContourPoint[] points) { } + } + [System.ComponentModel.ImmutableObjectAttribute(true)] + [System.Runtime.InteropServices.StructLayoutAttribute(System.Runtime.InteropServices.LayoutKind.Sequential)] + public partial struct ProsodyNumber : System.IEquatable + { + private object _dummy; + private int _dummyPrimitive; + public const int AbsoluteNumber = 2147483647; + public ProsodyNumber(int ssmlAttributeId) { throw null; } + public ProsodyNumber(float number) { throw null; } + public bool IsNumberPercent { get { throw null; } } + public float Number { get { throw null; } } + public int SsmlAttributeId { get { throw null; } } + public System.Speech.Synthesis.TtsEngine.ProsodyUnit Unit { get { throw null; } } + public override bool Equals(object obj) { throw null; } + public bool Equals(System.Speech.Synthesis.TtsEngine.ProsodyNumber other) { throw null; } + public override int GetHashCode() { throw null; } + public static bool operator ==(System.Speech.Synthesis.TtsEngine.ProsodyNumber prosodyNumber1, System.Speech.Synthesis.TtsEngine.ProsodyNumber prosodyNumber2) { throw null; } + public static bool operator !=(System.Speech.Synthesis.TtsEngine.ProsodyNumber prosodyNumber1, System.Speech.Synthesis.TtsEngine.ProsodyNumber prosodyNumber2) { throw null; } + } + public enum ProsodyPitch + { + Default = 0, + ExtraLow = 1, + Low = 2, + Medium = 3, + High = 4, + ExtraHigh = 5, + } + public enum ProsodyRange + { + Default = 0, + ExtraLow = 1, + Low = 2, + Medium = 3, + High = 4, + ExtraHigh = 5, + } + public enum ProsodyRate + { + Default = 0, + ExtraSlow = 1, + Slow = 2, + Medium = 3, + Fast = 4, + ExtraFast = 5, + } + public enum ProsodyUnit + { + Default = 0, + Hz = 1, + Semitone = 2, + } + public enum ProsodyVolume + { + ExtraLoud = -7, + Loud = -6, + Medium = -5, + Soft = -4, + ExtraSoft = -3, + Silent = -2, + Default = -1, + } + [System.Runtime.InteropServices.StructLayoutAttribute(System.Runtime.InteropServices.LayoutKind.Sequential)] + public partial class SayAs + { + public SayAs() { } + public string Detail { get { throw null; } set { } } + public string Format { get { throw null; } set { } } + public string InterpretAs { get { throw null; } set { } } + } + public partial class SkipInfo + { + public SkipInfo() { } + public int Count { get { throw null; } set { } } + public int Type { get { throw null; } set { } } + } + public enum SpeakOutputFormat + { + WaveFormat = 0, + Text = 1, + } + [System.ComponentModel.ImmutableObjectAttribute(true)] + [System.Runtime.InteropServices.StructLayoutAttribute(System.Runtime.InteropServices.LayoutKind.Sequential)] + public partial struct SpeechEventInfo : System.IEquatable + { + private object _dummy; + private int _dummyPrimitive; + public SpeechEventInfo(short eventId, short parameterType, int param1, System.IntPtr param2) { throw null; } + public short EventId { get { throw null; } } + public int Param1 { get { throw null; } } + public System.IntPtr Param2 { get { throw null; } } + public short ParameterType { get { throw null; } } + public override bool Equals(object obj) { throw null; } + public bool Equals(System.Speech.Synthesis.TtsEngine.SpeechEventInfo other) { throw null; } + public override int GetHashCode() { throw null; } + public static bool operator ==(System.Speech.Synthesis.TtsEngine.SpeechEventInfo event1, System.Speech.Synthesis.TtsEngine.SpeechEventInfo event2) { throw null; } + public static bool operator !=(System.Speech.Synthesis.TtsEngine.SpeechEventInfo event1, System.Speech.Synthesis.TtsEngine.SpeechEventInfo event2) { throw null; } + } + [System.Runtime.InteropServices.StructLayoutAttribute(System.Runtime.InteropServices.LayoutKind.Sequential)] + public partial class TextFragment + { + public TextFragment() { } + public System.Speech.Synthesis.TtsEngine.FragmentState State { get { throw null; } set { } } + public int TextLength { get { throw null; } set { } } + public int TextOffset { get { throw null; } set { } } + public string TextToSpeak { get { throw null; } set { } } + } + public enum TtsEngineAction + { + Speak = 0, + Silence = 1, + Pronounce = 2, + Bookmark = 3, + SpellOut = 4, + StartSentence = 5, + StartParagraph = 6, + ParseUnknownTag = 7, + } + public abstract partial class TtsEngineSsml + { + protected TtsEngineSsml(string registryKey) { } + public abstract void AddLexicon(System.Uri uri, string mediaType, System.Speech.Synthesis.TtsEngine.ITtsEngineSite site); + public abstract System.IntPtr GetOutputFormat(System.Speech.Synthesis.TtsEngine.SpeakOutputFormat speakOutputFormat, System.IntPtr targetWaveFormat); + public abstract void RemoveLexicon(System.Uri uri, System.Speech.Synthesis.TtsEngine.ITtsEngineSite site); + public abstract void Speak(System.Speech.Synthesis.TtsEngine.TextFragment[] fragment, System.IntPtr waveHeader, System.Speech.Synthesis.TtsEngine.ITtsEngineSite site); + } + public enum TtsEventId + { + StartInputStream = 1, + EndInputStream = 2, + VoiceChange = 3, + Bookmark = 4, + WordBoundary = 5, + Phoneme = 6, + SentenceBoundary = 7, + Viseme = 8, + AudioLevel = 9, + } +} diff --git a/src/libraries/System.Speech/ref/System.Speech.csproj b/src/libraries/System.Speech/ref/System.Speech.csproj new file mode 100644 index 00000000000000..c8543d1332372b --- /dev/null +++ b/src/libraries/System.Speech/ref/System.Speech.csproj @@ -0,0 +1,8 @@ + + + netstandard2.0 + + + + + \ No newline at end of file diff --git a/src/libraries/System.Speech/src/AudioFormat/AudioFormatConverter.cs b/src/libraries/System.Speech/src/AudioFormat/AudioFormatConverter.cs new file mode 100644 index 00000000000000..9911734ec95135 --- /dev/null +++ b/src/libraries/System.Speech/src/AudioFormat/AudioFormatConverter.cs @@ -0,0 +1,300 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Globalization; +using System.Runtime.InteropServices; +using System.Speech.AudioFormat; + +namespace System.Speech.Internal +{ + // Helper class which wraps AudioFormat and handles WaveFormatEx variable sized structure + internal static class AudioFormatConverter + { + #region Internal Methods + + internal static SpeechAudioFormatInfo ToSpeechAudioFormatInfo(IntPtr waveFormatPtr) + { + WaveFormatEx waveFormatEx = (WaveFormatEx)Marshal.PtrToStructure(waveFormatPtr, typeof(WaveFormatEx)); + + byte[] extraData = new byte[waveFormatEx.cbSize]; + IntPtr extraDataPtr = new(waveFormatPtr.ToInt64() + Marshal.SizeOf(waveFormatEx)); + for (int i = 0; i < waveFormatEx.cbSize; i++) + { + extraData[i] = Marshal.ReadByte(extraDataPtr, i); + } + + return new SpeechAudioFormatInfo((EncodingFormat)waveFormatEx.wFormatTag, (int)waveFormatEx.nSamplesPerSec, (short)waveFormatEx.wBitsPerSample, (short)waveFormatEx.nChannels, (int)waveFormatEx.nAvgBytesPerSec, (short)waveFormatEx.nBlockAlign, extraData); + } + + internal static SpeechAudioFormatInfo ToSpeechAudioFormatInfo(string formatString) + { + // Is it normal format? + short streamFormat; + if (short.TryParse(formatString, NumberStyles.None, CultureInfo.InvariantCulture, out streamFormat)) + { + // Now convert enum value into real info + return ConvertFormat((StreamFormat)streamFormat); + } + return null; + } + + #endregion + + #region Private Methods + + /// + /// This method converts the specified stream format into a wave format + /// + private static SpeechAudioFormatInfo ConvertFormat(StreamFormat eFormat) + { + WaveFormatEx waveEx = new(); + byte[] extra = null; + + if (eFormat >= StreamFormat.PCM_8kHz8BitMono && eFormat <= StreamFormat.PCM_48kHz16BitStereo) + { + uint index = (uint)(eFormat - StreamFormat.PCM_8kHz8BitMono); + bool isStereo = (index & 0x1) != 0; + bool is16 = (index & 0x2) != 0; + uint dwKHZ = (index & 0x3c) >> 2; + uint[] adwKHZ = new uint[] { 8000, 11025, 12000, 16000, 22050, 24000, 32000, 44100, 48000 }; + waveEx.wFormatTag = (ushort)WaveFormatId.Pcm; + waveEx.nChannels = waveEx.nBlockAlign = (ushort)(isStereo ? 2 : 1); + waveEx.nSamplesPerSec = adwKHZ[dwKHZ]; + waveEx.wBitsPerSample = 8; + if (is16) + { + waveEx.wBitsPerSample *= 2; + waveEx.nBlockAlign *= 2; + } + waveEx.nAvgBytesPerSec = waveEx.nSamplesPerSec * waveEx.nBlockAlign; + } + else if (eFormat == StreamFormat.TrueSpeech_8kHz1BitMono) + { + waveEx.wFormatTag = (ushort)WaveFormatId.TrueSpeech; + waveEx.nChannels = 1; + waveEx.nSamplesPerSec = 8000; + waveEx.nAvgBytesPerSec = 1067; + waveEx.nBlockAlign = 32; + waveEx.wBitsPerSample = 1; + waveEx.cbSize = 32; + extra = new byte[32]; + extra[0] = 1; + extra[2] = 0xF0; + } + else if ((eFormat >= StreamFormat.CCITT_ALaw_8kHzMono) && (eFormat <= StreamFormat.CCITT_ALaw_44kHzStereo)) + { + uint index = (uint)(eFormat - StreamFormat.CCITT_ALaw_8kHzMono); + uint dwKHZ = index / 2; + uint[] adwKHZ = { 8000, 11025, 22050, 44100 }; + bool isStereo = (index & 0x1) != 0; + waveEx.wFormatTag = (ushort)WaveFormatId.Alaw; + waveEx.nChannels = waveEx.nBlockAlign = (ushort)(isStereo ? 2 : 1); + waveEx.nSamplesPerSec = adwKHZ[dwKHZ]; + waveEx.wBitsPerSample = 8; + waveEx.nAvgBytesPerSec = waveEx.nSamplesPerSec * waveEx.nBlockAlign; + } + else if ((eFormat >= StreamFormat.CCITT_uLaw_8kHzMono) && + (eFormat <= StreamFormat.CCITT_uLaw_44kHzStereo)) + { + uint index = (uint)(eFormat - StreamFormat.CCITT_uLaw_8kHzMono); + uint dwKHZ = index / 2; + uint[] adwKHZ = new uint[] { 8000, 11025, 22050, 44100 }; + bool isStereo = (index & 0x1) != 0; + waveEx.wFormatTag = (ushort)WaveFormatId.Mulaw; + waveEx.nChannels = waveEx.nBlockAlign = (ushort)(isStereo ? 2 : 1); + waveEx.nSamplesPerSec = adwKHZ[dwKHZ]; + waveEx.wBitsPerSample = 8; + waveEx.nAvgBytesPerSec = waveEx.nSamplesPerSec * waveEx.nBlockAlign; + } + else if ((eFormat >= StreamFormat.ADPCM_8kHzMono) && + (eFormat <= StreamFormat.ADPCM_44kHzStereo)) + { + //--- Some of these values seem odd. We used what the codec told us. + uint[] adwKHZ = new uint[] { 8000, 11025, 22050, 44100 }; + uint[] BytesPerSec = new uint[] { 4096, 8192, 5644, 11289, 11155, 22311, 22179, 44359 }; + uint[] BlockAlign = new uint[] { 256, 256, 512, 1024 }; + byte[] Extra811 = new byte[32] + { + 0xF4, 0x01, 0x07, 0x00, 0x00, 0x01, 0x00, 0x00, + 0x00, 0x02, 0x00, 0xFF, 0x00, 0x00, 0x00, 0x00, + 0xC0, 0x00, 0x40, 0x00, 0xF0, 0x00, 0x00, 0x00, + 0xCC, 0x01, 0x30, 0xFF, 0x88, 0x01, 0x18, 0xFF + }; + + byte[] Extra22 = new byte[32] + { + 0xF4, 0x03, 0x07, 0x00, 0x00, 0x01, 0x00, 0x00, + 0x00, 0x02, 0x00, 0xFF, 0x00, 0x00, 0x00, 0x00, + 0xC0, 0x00, 0x40, 0x00, 0xF0, 0x00, 0x00, 0x00, + 0xCC, 0x01, 0x30, 0xFF, 0x88, 0x01, 0x18, 0xFF + }; + + byte[] Extra44 = new byte[32] + { + 0xF4, 0x07, 0x07, 0x00, 0x00, 0x01, 0x00, 0x00, + 0x00, 0x02, 0x00, 0xFF, 0x00, 0x00, 0x00, 0x00, + 0xC0, 0x00, 0x40, 0x00, 0xF0, 0x00, 0x00, 0x00, + 0xCC, 0x01, 0x30, 0xFF, 0x88, 0x01, 0x18, 0xFF + }; + + byte[][] Extra = new byte[][] { Extra811, Extra811, Extra22, Extra44 }; + uint index = (uint)(eFormat - StreamFormat.ADPCM_8kHzMono); + uint dwKHZ = index / 2; + bool isStereo = (index & 0x1) != 0; + waveEx.wFormatTag = (ushort)WaveFormatId.AdPcm; + waveEx.nChannels = (ushort)(isStereo ? 2 : 1); + waveEx.nSamplesPerSec = adwKHZ[dwKHZ]; + waveEx.nAvgBytesPerSec = BytesPerSec[index]; + waveEx.nBlockAlign = (ushort)(BlockAlign[dwKHZ] * waveEx.nChannels); + waveEx.wBitsPerSample = 4; + waveEx.cbSize = 32; + extra = (byte[])Extra[dwKHZ].Clone(); + } + else if ((eFormat >= StreamFormat.GSM610_8kHzMono) && + (eFormat <= StreamFormat.GSM610_44kHzMono)) + { + //--- Some of these values seem odd. We used what the codec told us. + uint[] adwKHZ = new uint[] { 8000, 11025, 22050, 44100 }; + uint[] BytesPerSec = new uint[] { 1625, 2239, 4478, 8957 }; + uint index = (uint)(eFormat - StreamFormat.GSM610_8kHzMono); + waveEx.wFormatTag = (ushort)WaveFormatId.Gsm610; + waveEx.nChannels = 1; + waveEx.nSamplesPerSec = adwKHZ[index]; + waveEx.nAvgBytesPerSec = BytesPerSec[index]; + waveEx.nBlockAlign = 65; + waveEx.wBitsPerSample = 0; + waveEx.cbSize = 2; + extra = new byte[2]; + extra[0] = 0x40; + extra[1] = 0x01; + } + else + { + waveEx = null; + switch (eFormat) + { + case StreamFormat.NoAssignedFormat: + break; + + case StreamFormat.Text: + break; + + default: + throw new FormatException(); + } + } + + return waveEx != null ? new SpeechAudioFormatInfo((EncodingFormat)waveEx.wFormatTag, (int)waveEx.nSamplesPerSec, waveEx.wBitsPerSample, waveEx.nChannels, (int)waveEx.nAvgBytesPerSec, waveEx.nBlockAlign, extra) : null; + } + + private enum StreamFormat + { + Default = -1, + NoAssignedFormat = 0, // Similar to GUID_NULL + Text, + NonStandardFormat, // Non-SAPI 5.1 standard format with no WAVEFORMATEX description + ExtendedAudioFormat, // Non-SAPI 5.1 standard format but has WAVEFORMATEX description + // Standard PCM wave formats + PCM_8kHz8BitMono, + PCM_8kHz8BitStereo, + PCM_8kHz16BitMono, + PCM_8kHz16BitStereo, + PCM_11kHz8BitMono, + PCM_11kHz8BitStereo, + PCM_11kHz16BitMono, + PCM_11kHz16BitStereo, + PCM_12kHz8BitMono, + PCM_12kHz8BitStereo, + PCM_12kHz16BitMono, + PCM_12kHz16BitStereo, + PCM_16kHz8BitMono, + PCM_16kHz8BitStereo, + PCM_16kHz16BitMono, + PCM_16kHz16BitStereo, + PCM_22kHz8BitMono, + PCM_22kHz8BitStereo, + PCM_22kHz16BitMono, + PCM_22kHz16BitStereo, + PCM_24kHz8BitMono, + PCM_24kHz8BitStereo, + PCM_24kHz16BitMono, + PCM_24kHz16BitStereo, + PCM_32kHz8BitMono, + PCM_32kHz8BitStereo, + PCM_32kHz16BitMono, + PCM_32kHz16BitStereo, + PCM_44kHz8BitMono, + PCM_44kHz8BitStereo, + PCM_44kHz16BitMono, + PCM_44kHz16BitStereo, + PCM_48kHz8BitMono, + PCM_48kHz8BitStereo, + PCM_48kHz16BitMono, + PCM_48kHz16BitStereo, + // TrueSpeech format + + TrueSpeech_8kHz1BitMono, + // A-Law formats + CCITT_ALaw_8kHzMono, + CCITT_ALaw_8kHzStereo, + CCITT_ALaw_11kHzMono, + CCITT_ALaw_11kHzStereo, + CCITT_ALaw_22kHzMono, + CCITT_ALaw_22kHzStereo, + CCITT_ALaw_44kHzMono, + CCITT_ALaw_44kHzStereo, + // u-Law formats + CCITT_uLaw_8kHzMono, + CCITT_uLaw_8kHzStereo, + CCITT_uLaw_11kHzMono, + CCITT_uLaw_11kHzStereo, + CCITT_uLaw_22kHzMono, + CCITT_uLaw_22kHzStereo, + CCITT_uLaw_44kHzMono, + CCITT_uLaw_44kHzStereo, + // ADPCM formats + ADPCM_8kHzMono, + ADPCM_8kHzStereo, + ADPCM_11kHzMono, + ADPCM_11kHzStereo, + ADPCM_22kHzMono, + ADPCM_22kHzStereo, + ADPCM_44kHzMono, + ADPCM_44kHzStereo, + // GSM 6.10 formats + GSM610_8kHzMono, + GSM610_11kHzMono, + GSM610_22kHzMono, + GSM610_44kHzMono, + NUM_FORMATS + } + + #endregion + + #region Private Type + + private enum WaveFormatId + { + Pcm = 1, + AdPcm = 0x0002, + TrueSpeech = 0x0022, + Alaw = 0x0006, + Mulaw = 0x0007, + Gsm610 = 0x0031 + } + + [StructLayout(LayoutKind.Sequential)] + private class WaveFormatEx + { + public ushort wFormatTag; + public ushort nChannels; + public uint nSamplesPerSec; + public uint nAvgBytesPerSec; + public ushort nBlockAlign; + public ushort wBitsPerSample; + public ushort cbSize; + } + + #endregion + } +} diff --git a/src/libraries/System.Speech/src/AudioFormat/EncodingFormat.cs b/src/libraries/System.Speech/src/AudioFormat/EncodingFormat.cs new file mode 100644 index 00000000000000..210ce1d313ee01 --- /dev/null +++ b/src/libraries/System.Speech/src/AudioFormat/EncodingFormat.cs @@ -0,0 +1,13 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace System.Speech.AudioFormat +{ + // These enumeration values are the same values used in the WAVEFORMATEX structure used in wave files. + public enum EncodingFormat + { + Pcm = 0x0001, + ALaw = 0x0006, + ULaw = 0x0007 + } +} diff --git a/src/libraries/System.Speech/src/AudioFormat/SpeechAudioFormatInfo.cs b/src/libraries/System.Speech/src/AudioFormat/SpeechAudioFormatInfo.cs new file mode 100644 index 00000000000000..d107b9bf340e71 --- /dev/null +++ b/src/libraries/System.Speech/src/AudioFormat/SpeechAudioFormatInfo.cs @@ -0,0 +1,192 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.ComponentModel; +using System.Speech.Internal.Synthesis; + +namespace System.Speech.AudioFormat +{ + [Serializable] + public class SpeechAudioFormatInfo + { + #region Constructors + + private SpeechAudioFormatInfo(EncodingFormat encodingFormat, int samplesPerSecond, short bitsPerSample, short channelCount, byte[] formatSpecificData) + { + if (encodingFormat == 0) + { + throw new ArgumentException(SR.Get(SRID.CannotUseCustomFormat), nameof(encodingFormat)); + } + if (samplesPerSecond <= 0) + { + throw new ArgumentOutOfRangeException(nameof(samplesPerSecond), SR.Get(SRID.MustBeGreaterThanZero)); + } + if (bitsPerSample <= 0) + { + throw new ArgumentOutOfRangeException(nameof(bitsPerSample), SR.Get(SRID.MustBeGreaterThanZero)); + } + if (channelCount <= 0) + { + throw new ArgumentOutOfRangeException(nameof(channelCount), SR.Get(SRID.MustBeGreaterThanZero)); + } + + _encodingFormat = encodingFormat; + _samplesPerSecond = samplesPerSecond; + _bitsPerSample = bitsPerSample; + _channelCount = channelCount; + if (formatSpecificData == null) + { + _formatSpecificData = Array.Empty(); + } + else + { + _formatSpecificData = (byte[])formatSpecificData.Clone(); + } + + switch (encodingFormat) + { + case EncodingFormat.ALaw: + case EncodingFormat.ULaw: + if (bitsPerSample != 8) + { + throw new ArgumentOutOfRangeException(nameof(bitsPerSample)); + } + if (formatSpecificData != null && formatSpecificData.Length != 0) + { + throw new ArgumentOutOfRangeException(nameof(formatSpecificData)); + } + break; + } + } + [EditorBrowsable(EditorBrowsableState.Advanced)] + public SpeechAudioFormatInfo(EncodingFormat encodingFormat, int samplesPerSecond, int bitsPerSample, int channelCount, int averageBytesPerSecond, int blockAlign, byte[] formatSpecificData) + : this(encodingFormat, samplesPerSecond, (short)bitsPerSample, (short)channelCount, formatSpecificData) + { + // Don't explicitly check these are sensible values - allow flexibility here as some formats may do unexpected things here. + if (averageBytesPerSecond <= 0) + { + throw new ArgumentOutOfRangeException(nameof(averageBytesPerSecond), SR.Get(SRID.MustBeGreaterThanZero)); + } + if (blockAlign <= 0) + { + throw new ArgumentOutOfRangeException(nameof(blockAlign), SR.Get(SRID.MustBeGreaterThanZero)); + } + _averageBytesPerSecond = averageBytesPerSecond; + _blockAlign = (short)blockAlign; + } + public SpeechAudioFormatInfo(int samplesPerSecond, AudioBitsPerSample bitsPerSample, AudioChannel channel) + : this(EncodingFormat.Pcm, samplesPerSecond, (short)bitsPerSample, (short)channel, null) + { + // Don't explicitly check these are sensible values - allow flexibility here as some formats may do unexpected things here. + _blockAlign = (short)(_channelCount * (_bitsPerSample / 8)); + _averageBytesPerSecond = _samplesPerSecond * _blockAlign; + } + + #endregion + + #region Public Properties + [EditorBrowsable(EditorBrowsableState.Advanced)] + public int AverageBytesPerSecond { get { return _averageBytesPerSecond; } } + [EditorBrowsable(EditorBrowsableState.Advanced)] + public int BitsPerSample { get { return _bitsPerSample; } } + [EditorBrowsable(EditorBrowsableState.Advanced)] + public int BlockAlign { get { return _blockAlign; } } + public EncodingFormat EncodingFormat { get { return _encodingFormat; } } + public int ChannelCount { get { return _channelCount; } } + public int SamplesPerSecond { get { return _samplesPerSecond; } } + + #endregion + + #region Public Methods + public byte[] FormatSpecificData() { return (byte[])_formatSpecificData.Clone(); } + public override bool Equals(object obj) + { + SpeechAudioFormatInfo refObj = obj as SpeechAudioFormatInfo; + if (refObj == null) + { + return false; + } + + if (!(_averageBytesPerSecond.Equals(refObj._averageBytesPerSecond) && + _bitsPerSample.Equals(refObj._bitsPerSample) && + _blockAlign.Equals(refObj._blockAlign) && + _encodingFormat.Equals(refObj._encodingFormat) && + _channelCount.Equals(refObj._channelCount) && + _samplesPerSecond.Equals(refObj._samplesPerSecond))) + { + return false; + } + if (_formatSpecificData.Length != refObj._formatSpecificData.Length) + { + return false; + } + for (int i = 0; i < _formatSpecificData.Length; i++) + { + if (_formatSpecificData[i] != refObj._formatSpecificData[i]) + { + return false; + } + } + return true; + } + public override int GetHashCode() + { + return _averageBytesPerSecond.GetHashCode(); + } + + #endregion + + #region Internal Methods + internal byte[] WaveFormat + { + get + { + WAVEFORMATEX wfx = new(); + wfx.wFormatTag = (short)EncodingFormat; + wfx.nChannels = (short)ChannelCount; + wfx.nSamplesPerSec = SamplesPerSecond; + wfx.nAvgBytesPerSec = AverageBytesPerSecond; + wfx.nBlockAlign = (short)BlockAlign; + wfx.wBitsPerSample = (short)BitsPerSample; + wfx.cbSize = (short)FormatSpecificData().Length; + + byte[] abWfx = wfx.ToBytes(); + if (wfx.cbSize > 0) + { + byte[] wfxTemp = new byte[abWfx.Length + wfx.cbSize]; + Array.Copy(abWfx, wfxTemp, abWfx.Length); + Array.Copy(FormatSpecificData(), 0, wfxTemp, abWfx.Length, wfx.cbSize); + abWfx = wfxTemp; + } + return abWfx; + } + } + #endregion + + #region Private Fields + + private int _averageBytesPerSecond; + private short _bitsPerSample; + private short _blockAlign; + private EncodingFormat _encodingFormat; + private short _channelCount; + private int _samplesPerSecond; + private byte[] _formatSpecificData; + + #endregion + } + + #region Public Properties + public enum AudioChannel + { + Mono = 1, + Stereo = 2 + } + public enum AudioBitsPerSample + { + Eight = 8, + Sixteen = 16 + } + + #endregion +} diff --git a/src/libraries/System.Speech/src/Internal/AlphabetConverter.cs b/src/libraries/System.Speech/src/Internal/AlphabetConverter.cs new file mode 100644 index 00000000000000..ee822252c40e2e --- /dev/null +++ b/src/libraries/System.Speech/src/Internal/AlphabetConverter.cs @@ -0,0 +1,342 @@ +// 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; +using System.Diagnostics; +using System.IO; +using System.Reflection; +using System.Text; +using Microsoft.Win32; + +namespace System.Speech.Internal +{ + internal enum AlphabetType + { + Sapi, Ipa, Ups + } + + /// + /// This class allows conversion between SAPI and IPA phonemes. + /// Objects of this class are not thread safe for modifying state. + /// + internal class AlphabetConverter + { + #region Constructors + + internal AlphabetConverter(int langId) + { + _currentLangId = -1; + SetLanguageId(langId); + } + + #endregion + + #region internal Methods + + /// + /// Convert from SAPI phonemes to IPA phonemes. + /// + /// + /// Return an array of unicode characters each of which represents an IPA phoneme if the SAPI phonemes are valid. + /// Otherwise, return null. + /// + internal char[] SapiToIpa(char[] phonemes) + { + return Convert(phonemes, true); + } + + /// + /// Convert from IPA phonemes to SAPI phonemes. + /// + /// Return an array of unicode characters each of which represents a SAPI phoneme if the IPA phonemes are valid. + /// Otherwise, return null. + internal char[] IpaToSapi(char[] phonemes) + { + return Convert(phonemes, false); + } + + /// + /// Determines whether a given string of SAPI ids can be potentially converted using a single + /// conversion unit, that is, a prefix of some convertible string. + /// + /// The string of SAPI or UPS phoneme ids + /// To indicate whether parameter phonemes is in SAPI or UPS phonemes + internal bool IsPrefix(string phonemes, bool isSapi) + { + if (_phoneMap == null) + return false; + + return _phoneMap.IsPrefix(phonemes, isSapi); + } + + internal bool IsConvertibleUnit(string phonemes, bool isSapi) + { + if (_phoneMap == null) + return false; + + return _phoneMap.ConvertPhoneme(phonemes, isSapi) != null; + } + + internal int SetLanguageId(int langId) + { + if (langId < 0) + { + throw new ArgumentException(SR.Get(SRID.MustBeGreaterThanZero), nameof(langId)); + } + if (langId == _currentLangId) + { + return _currentLangId; + } + + int i; + int oldLangId = _currentLangId; + for (i = 0; i < s_langIds.Length; i++) + { + if (s_langIds[i] == langId) + { + break; + } + } + if (i == s_langIds.Length) + { + //Debug.Fail($"No phoneme map for LCID {langId}, maps exist for {string.Join(',', s_langIds)}\n"); + _currentLangId = langId; + _phoneMap = null; + } + else + { + lock (s_staticLock) + { + if (s_phoneMaps[i] == null) + { + s_phoneMaps[i] = CreateMap(s_resourceNames[i]); + } + _phoneMap = s_phoneMaps[i]; + _currentLangId = langId; + } + } + return oldLangId; + } + #endregion + + #region Private Methods + + private char[] Convert(char[] phonemes, bool isSapi) + { + // If the phoneset of the selected language is UPS anyway, that is phone mapping is unnecessary, + // we return the same phoneme string. But we still need to make a copy. + if (_phoneMap == null || phonemes.Length == 0) + { + return (char[])phonemes.Clone(); + } + + // + // We break the phoneme string into substrings of phonemes, each of which is directly convertible from + // the mapping table. If there is ambiguity, we always choose the largest substring as we go from left + // to right. + // + // In order to do this, we check whether a given substring is a potential prefix of a convertible substring. + // + + StringBuilder result = new(); + int startIndex; // Starting index of a substring being considered + int endIndex; // The ending index of the last convertible substring + string token; // Holds a substring of phonemes that are directly convertible from the mapping table. + string lastConvert; // Holds last convertible substring, starting from startIndex. + + string tempConvert; + string source = new(phonemes); + int i; + + lastConvert = null; + startIndex = i = 0; + endIndex = -1; + + while (i < source.Length) + { + token = source.Substring(startIndex, i - startIndex + 1); + if (_phoneMap.IsPrefix(token, isSapi)) + { + tempConvert = _phoneMap.ConvertPhoneme(token, isSapi); + // Note we may have an empty string for conversion result here + if (tempConvert != null) + { + lastConvert = tempConvert; + endIndex = i; + } + } + else + { + // If we have not had a convertible substring, the input is not convertible. + if (lastConvert == null) + { + break; + } + else + { + // Use the converted substring, and start over from the last convertible position. + result.Append(lastConvert); + i = endIndex; + startIndex = endIndex + 1; + lastConvert = null; + } + } + i++; + } + + if (lastConvert != null && endIndex == phonemes.Length - 1) + { + result.Append(lastConvert); + } + else + { + return null; + } + + return result.ToString().ToCharArray(); + } + + private PhoneMapData CreateMap(string resourceName) + { + Assembly assembly = Assembly.GetAssembly(GetType()); + Stream stream = assembly.GetManifestResourceStream(resourceName); + if (stream == null) + { + throw new FileLoadException(SR.Get(SRID.CannotLoadResourceFromManifest, resourceName, assembly.FullName)); + } + return new PhoneMapData(new BufferedStream(stream)); + } + + #endregion + + #region Private Fields + + private int _currentLangId; + private PhoneMapData _phoneMap; + + private static int[] s_langIds = new int[] { 0x804, 0x404, 0x407, 0x409, 0x40A, 0x40C, 0x411 }; + private static string[] s_resourceNames = + new string[] { "upstable_chs.upsmap", "upstable_cht.upsmap", "upstable_deu.upsmap", "upstable_enu.upsmap", + "upstable_esp.upsmap", "upstable_fra.upsmap", "upstable_jpn.upsmap", +}; + private static PhoneMapData[] s_phoneMaps = new PhoneMapData[7]; + private static object s_staticLock = new(); + + #endregion + + #region Private Type + + internal class PhoneMapData + { + private class ConversionUnit + { + public string sapi; + public string ups; + public bool isDefault; + } + + internal PhoneMapData(Stream input) + { + using (BinaryReader reader = new(input, System.Text.Encoding.Unicode)) + { + int size = reader.ReadInt32(); + _convertTable = new ConversionUnit[size]; + int i; + for (i = 0; i < size; i++) + { + _convertTable[i] = new ConversionUnit + { + sapi = ReadPhoneString(reader), + ups = ReadPhoneString(reader), + isDefault = reader.ReadInt32() != 0 ? true : false + }; + } + + _prefixSapiTable = InitializePrefix(true); + _prefixUpsTable = InitializePrefix(false); + } + } + + internal bool IsPrefix(string prefix, bool isSapi) + { + if (isSapi) + { + return _prefixSapiTable.ContainsKey(prefix); + } + else + { + return _prefixUpsTable.ContainsKey(prefix); + } + } + + internal string ConvertPhoneme(string phoneme, bool isSapi) + { + ConversionUnit unit; + if (isSapi) + { + unit = (ConversionUnit)_prefixSapiTable[phoneme]; + } + else + { + unit = (ConversionUnit)_prefixUpsTable[phoneme]; + } + if (unit == null) + { + return null; + } + return isSapi ? unit.ups : unit.sapi; + } + + /// + /// Create a hash table of all possible prefix substrings for each ConversionUnit + /// + /// Creating a SAPI or UPS prefix table + private Hashtable InitializePrefix(bool isSapi) + { + int i, j; + Hashtable prefixTable = Hashtable.Synchronized(new Hashtable()); + string from, key; + for (i = 0; i < _convertTable.Length; i++) + { + if (isSapi) + { + from = _convertTable[i].sapi; + } + else + { + from = _convertTable[i].ups; + } + + for (j = 0; j + 1 < from.Length; j++) + { + key = from.Substring(0, j + 1); + if (!prefixTable.ContainsKey(key)) + { + prefixTable[key] = null; + } + } + + if (_convertTable[i].isDefault || prefixTable[from] == null) + { + prefixTable[from] = _convertTable[i]; + } + } + return prefixTable; + } + + private static string ReadPhoneString(BinaryReader reader) + { + int phoneLength; + char[] phoneString; + phoneLength = reader.ReadInt16() / 2; + phoneString = reader.ReadChars(phoneLength); + return new string(phoneString, 0, phoneLength - 1); + } + + private Hashtable _prefixSapiTable, _prefixUpsTable; + private ConversionUnit[] _convertTable; + } + + #endregion + } +} diff --git a/src/libraries/System.Speech/src/Internal/AsyncSerializedWorker.cs b/src/libraries/System.Speech/src/Internal/AsyncSerializedWorker.cs new file mode 100644 index 00000000000000..5396cd09d6c2e0 --- /dev/null +++ b/src/libraries/System.Speech/src/Internal/AsyncSerializedWorker.cs @@ -0,0 +1,282 @@ +// 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; +using System.ComponentModel; +using System.Text; +using System.Threading; + +namespace System.Speech.Internal +{ + internal interface IAsyncDispatch + { + void Post(object evt); + void Post(object[] evt); + void PostOperation(Delegate callback, params object[] parameters); + } + + internal class AsyncSerializedWorker : IAsyncDispatch + { + #region Constructors + + internal AsyncSerializedWorker(WaitCallback defaultCallback, SynchronizationContext syncContext) + { + _syncContext = syncContext; + _workerPostCallback = new SendOrPostCallback(WorkerProc); + Initialize(defaultCallback); + } + + private void Initialize(WaitCallback defaultCallback) + { + _queue = new Queue(); + _hasPendingPost = false; + _workerCallback = new WaitCallback(WorkerProc); + _defaultCallback = defaultCallback; + _isAsyncMode = true; + _isEnabled = true; + } + + #endregion + + #region Public Methods + + public void Post(object evt) + { + AddItem(new AsyncWorkItem(DefaultCallback, evt)); + } + + public void Post(object[] evt) + { + int i; + lock (_queue.SyncRoot) + { + if (Enabled) + { + for (i = 0; i < evt.Length; i++) + { + AddItem(new AsyncWorkItem(DefaultCallback, evt[i])); + } + } + } + } + + public void PostOperation(Delegate callback, params object[] parameters) + { + AddItem(new AsyncWorkItem(callback, parameters)); + } + + #endregion + + #region Internal Properties and Methods + + internal bool Enabled + { + get + { + lock (_queue.SyncRoot) + { + return _isEnabled; + } + } + set + { + lock (_queue.SyncRoot) + { + _isEnabled = value; + } + } + } + + internal void Purge() + { + lock (_queue.SyncRoot) + { + _queue.Clear(); + } + } + + internal WaitCallback DefaultCallback + { + get + { + lock (_queue.SyncRoot) + { + return _defaultCallback; + } + } + } + + internal AsyncWorkItem NextWorkItem() + { + lock (_queue.SyncRoot) + { + if (_queue.Count == 0) + { + return null; + } + else + { + AsyncWorkItem workItem = (AsyncWorkItem)_queue.Dequeue(); + if (_queue.Count == 0) + { + _hasPendingPost = false; + } + return workItem; + } + } + } + + internal void ConsumeQueue() + { + AsyncWorkItem workItem; + while (null != (workItem = NextWorkItem())) + { + workItem.Invoke(); + } + } + + internal bool AsyncMode + { + get + { + lock (_queue.SyncRoot) + { + return _isAsyncMode; + } + } + set + { + bool notify = false; + lock (_queue.SyncRoot) + { + if (_isAsyncMode != value) + { + _isAsyncMode = value; + if (_queue.Count > 0) + { + notify = true; + } + } + } + + // We need to resume the worker thread if there are post-events to process + if (notify) + { + OnWorkItemPending(); + } + } + } + + // event handler of this event should execute quickly and must not acquire any lock + internal event WaitCallback WorkItemPending; + + #endregion + #region Private/Protected Methods + + private void AddItem(AsyncWorkItem item) + { + bool processing = true; + lock (_queue.SyncRoot) + { + if (Enabled) + { + _queue.Enqueue(item); + if (!_hasPendingPost || !_isAsyncMode) + { + processing = false; + _hasPendingPost = true; + } + } + } + + if (!processing) + { + OnWorkItemPending(); + } + } + + private void WorkerProc(object ignored) + { + AsyncWorkItem workItem; + while (true) + { + lock (_queue.SyncRoot) + { + if (_queue.Count > 0 && _isAsyncMode) + { + workItem = (AsyncWorkItem)_queue.Dequeue(); + } + else + { + if (_queue.Count == 0) + { + _hasPendingPost = false; + } + break; + } + } + + workItem.Invoke(); + } + } + + private void OnWorkItemPending() + { + // No need to lock here + if (_hasPendingPost) + { + if (AsyncMode) + { + if (_syncContext == null) + { + ThreadPool.QueueUserWorkItem(_workerCallback, null); + } + else + { + _syncContext.Post(_workerPostCallback, null); + } + } + else if (WorkItemPending != null) + { + WorkItemPending(null); + } + } + } + + #endregion + + #region Private Fields + + private SynchronizationContext _syncContext; + private SendOrPostCallback _workerPostCallback; + + private Queue _queue; + private bool _hasPendingPost; + private bool _isAsyncMode; + private WaitCallback _workerCallback; + private WaitCallback _defaultCallback; + private bool _isEnabled; + + #endregion + } + + internal class AsyncWorkItem + { + internal AsyncWorkItem(Delegate dynamicCallback, params object[] postData) + { + _dynamicCallback = dynamicCallback; + _postData = postData; + } + + internal void Invoke() + { + if (_dynamicCallback != null) + { + _dynamicCallback.DynamicInvoke(_postData); + } + } + + private Delegate _dynamicCallback; + private object[] _postData; + } +} diff --git a/src/libraries/System.Speech/src/Internal/GrammarBuilding/BuilderElements.cs b/src/libraries/System.Speech/src/Internal/GrammarBuilding/BuilderElements.cs new file mode 100644 index 00000000000000..04947f11aa09c8 --- /dev/null +++ b/src/libraries/System.Speech/src/Internal/GrammarBuilding/BuilderElements.cs @@ -0,0 +1,274 @@ +// 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.Diagnostics; +using System.Speech.Internal.SrgsParser; +using System.Speech.Recognition; +using System.Text; + +namespace System.Speech.Internal.GrammarBuilding +{ + [DebuggerDisplay("{DebugSummary}")] + internal abstract class BuilderElements : GrammarBuilderBase + { + #region Constructors + + internal BuilderElements() + { + } + + #endregion + + #region Public Methods + public override bool Equals(object obj) + { + BuilderElements refObj = obj as BuilderElements; + if (refObj == null) + { + return false; + } + + // Easy out if the number of elements do not match + if (refObj.Count != Count || refObj.Items.Count != Items.Count) + { + return false; + } + + // Deep recursive search for equality + for (int i = 0; i < Items.Count; i++) + { + if (!Items[i].Equals(refObj.Items[i])) + { + return false; + } + } + return true; + } + public override int GetHashCode() + { + return base.GetHashCode(); + } + + #endregion + + #region Internal Methods + + /// + /// Optimization for a element tree + /// + protected void Optimize(Collection newRules) + { + // Create an dictionary of [Count of elements, list of elements] + SortedDictionary> dict = new(); + GetDictionaryElements(dict); + + // The dictionary is sorted from the smallest buckets to the largest. + // Revert the order in the keys arrays + int[] keys = new int[dict.Keys.Count]; + + int index = keys.Length - 1; + foreach (int key in dict.Keys) + { + keys[index--] = key; + } + + // Look for each bucket from the largest to the smallest + for (int i = 0; i < keys.Length && keys[i] >= 3; i++) + { + Collection gb = dict[keys[i]]; + for (int j = 0; j < gb.Count; j++) + { + RuleElement newRule = null; + RuleRefElement ruleRef = null; + for (int k = j + 1; k < gb.Count; k++) + { + if (gb[j] != null && gb[j].Equals(gb[k])) + { + BuilderElements current = gb[k]; + BuilderElements parent = current.Parent; + if (current is SemanticKeyElement) + // if current is already a ruleref. There is no need to create a new one + { + // Simply set the ruleref of the current element to the ruleref of the org element. + parent.Items[parent.Items.IndexOf(current)] = gb[j]; + } + else + { + // Create a rule to store the common elements + if (newRule == null) + { + newRule = new RuleElement(current, "_"); + newRules.Add(newRule); + } + + // Create a ruleref and attach the + if (ruleRef == null) + { + ruleRef = new RuleRefElement(newRule); + gb[j].Parent.Items[gb[j].Parent.Items.IndexOf(gb[j])] = ruleRef; + } + parent.Items[current.Parent.Items.IndexOf(current)] = ruleRef; + } + // + current.RemoveDictionaryElements(dict); + gb[k] = null; + } + } + } + } + } + + #endregion + + #region Internal Methods + + internal void Add(string phrase) + { + _items.Add(new GrammarBuilderPhrase(phrase)); + } + + internal void Add(GrammarBuilder builder) + { + foreach (GrammarBuilderBase item in builder.InternalBuilder.Items) + { + _items.Add(item); + } + } + + internal void Add(GrammarBuilderBase item) + { + _items.Add(item); + } + + internal void CloneItems(BuilderElements builders) + { + foreach (GrammarBuilderBase item in builders.Items) + { + _items.Add(item); + } + } + + internal void CreateChildrenElements(IElementFactory elementFactory, IRule parent, IdentifierCollection ruleIds) + { + foreach (GrammarBuilderBase buider in Items) + { + IElement element = buider.CreateElement(elementFactory, parent, parent, ruleIds); + if (element != null) + { + element.PostParse(parent); + elementFactory.AddElement(parent, element); + } + } + } + + internal void CreateChildrenElements(IElementFactory elementFactory, IItem parent, IRule rule, IdentifierCollection ruleIds) + { + foreach (GrammarBuilderBase buider in Items) + { + IElement element = buider.CreateElement(elementFactory, parent, rule, ruleIds); + if (element != null) + { + element.PostParse(parent); + elementFactory.AddElement(parent, element); + } + } + } + + internal override int CalcCount(BuilderElements parent) + { + base.CalcCount(parent); + int c = 1; + foreach (GrammarBuilderBase item in Items) + { + c += item.CalcCount(this); + } + Count = c; + + return c; + } + + #endregion + + #region Internal Properties + + internal List Items + { + get + { + return _items; + } + } + + internal override string DebugSummary + { + get + { + StringBuilder sb = new(); + + foreach (GrammarBuilderBase item in _items) + { + if (sb.Length > 0) + { + sb.Append(' '); + } + sb.Append(item.DebugSummary); + } + return sb.ToString(); + } + } + + #endregion + + #region Private Method + + private void GetDictionaryElements(SortedDictionary> dict) + { + // Recursive search from a matching subtree + foreach (GrammarBuilderBase item in Items) + { + BuilderElements current = item as BuilderElements; + + // Go deeper if the number of children is greater the element to compare against. + if (current != null) + { + if (!dict.ContainsKey(current.Count)) + { + dict.Add(current.Count, new Collection()); + } + dict[current.Count].Add(current); + + current.GetDictionaryElements(dict); + } + } + } + + private void RemoveDictionaryElements(SortedDictionary> dict) + { + // Recursive search from a matching subtree + foreach (GrammarBuilderBase item in Items) + { + BuilderElements current = item as BuilderElements; + + // Go deeper if the number of children is greater the element to compare against. + if (current != null) + { + // Recursively remove all elements + current.RemoveDictionaryElements(dict); + + dict[current.Count].Remove(current); + } + } + } + + #endregion + + #region Private Fields + + // List of builder elements + private readonly List _items = new(); + + #endregion + } +} diff --git a/src/libraries/System.Speech/src/Internal/GrammarBuilding/GrammarBuilderBase.cs b/src/libraries/System.Speech/src/Internal/GrammarBuilding/GrammarBuilderBase.cs new file mode 100644 index 00000000000000..bbd16e74a56050 --- /dev/null +++ b/src/libraries/System.Speech/src/Internal/GrammarBuilding/GrammarBuilderBase.cs @@ -0,0 +1,91 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Speech.Internal.SrgsParser; + +namespace System.Speech.Internal.GrammarBuilding +{ + + internal abstract class GrammarBuilderBase + { + #region Internal Methods + + internal abstract GrammarBuilderBase Clone(); + + internal abstract IElement CreateElement(IElementFactory elementFactory, IElement parent, IRule rule, IdentifierCollection ruleIds); + + internal virtual int CalcCount(BuilderElements parent) + { + Marked = false; + Parent = parent; + return Count; + } + + #endregion + + #region Internal Properties + + /// + /// Used by the GrammarBuilder optimizer to count the number of children and descendant for + /// an element + /// + internal virtual int Count + { + get + { + return _count; + } + + set + { + _count = value; + } + } + + /// + /// Marker to know if an element has already been visited. + /// + internal virtual bool Marked + { + get + { + return _marker; + } + + set + { + _marker = value; + } + } + + /// + /// Marker to know if an element has already been visited. + /// + internal virtual BuilderElements Parent + { + get + { + return _parent; + } + + set + { + _parent = value; + } + } + + internal abstract string DebugSummary { get; } + + #endregion + + #region Private Fields + + private int _count = 1; + + private bool _marker; + + private BuilderElements _parent; + + #endregion + } +} diff --git a/src/libraries/System.Speech/src/Internal/GrammarBuilding/GrammarBuilderDictation.cs b/src/libraries/System.Speech/src/Internal/GrammarBuilding/GrammarBuilderDictation.cs new file mode 100644 index 00000000000000..22f025beb7b8d2 --- /dev/null +++ b/src/libraries/System.Speech/src/Internal/GrammarBuilding/GrammarBuilderDictation.cs @@ -0,0 +1,94 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Speech.Internal.SrgsParser; + +namespace System.Speech.Internal.GrammarBuilding +{ + internal sealed class GrammarBuilderDictation : GrammarBuilderBase + { + #region Constructors + + internal GrammarBuilderDictation() + : this(null) + { + } + + internal GrammarBuilderDictation(string category) + { + _category = category; + } + + #endregion + + #region Public Methods + public override bool Equals(object obj) + { + GrammarBuilderDictation refObj = obj as GrammarBuilderDictation; + if (refObj == null) + { + return false; + } + return _category == refObj._category; + } + public override int GetHashCode() + { + return _category == null ? 0 : _category.GetHashCode(); + } + + #endregion + + #region Internal Methods + + internal override GrammarBuilderBase Clone() + { + return new GrammarBuilderDictation(_category); + } + + internal override IElement CreateElement(IElementFactory elementFactory, IElement parent, IRule rule, IdentifierCollection ruleIds) + { + // Return the IRuleRef to the dictation grammar + return CreateRuleRefToDictation(elementFactory, parent); + } + + #endregion + + #region Internal Properties + + internal override string DebugSummary + { + get + { + string category = _category != null ? ":" + _category : string.Empty; + return "dictation" + category; + } + } + + #endregion + + #region Private Methods + + private IRuleRef CreateRuleRefToDictation(IElementFactory elementFactory, IElement parent) + { + Uri ruleUri; + if (!string.IsNullOrEmpty(_category) && _category == "spelling") + { + ruleUri = new Uri("grammar:dictation#spelling", UriKind.RelativeOrAbsolute); + } + else + { + ruleUri = new Uri("grammar:dictation", UriKind.RelativeOrAbsolute); + } + + return elementFactory.CreateRuleRef(parent, ruleUri, null, null); + } + + #endregion + + #region Private Fields + + private readonly string _category; + + #endregion + } +} diff --git a/src/libraries/System.Speech/src/Internal/GrammarBuilding/GrammarBuilderPhrase.cs b/src/libraries/System.Speech/src/Internal/GrammarBuilding/GrammarBuilderPhrase.cs new file mode 100644 index 00000000000000..c0640b2501086e --- /dev/null +++ b/src/libraries/System.Speech/src/Internal/GrammarBuilding/GrammarBuilderPhrase.cs @@ -0,0 +1,133 @@ +// 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; +using System.Speech.Internal.SrgsCompiler; +using System.Speech.Internal.SrgsParser; +using System.Speech.Recognition; + +namespace System.Speech.Internal.GrammarBuilding +{ + [DebuggerDisplay("{DebugSummary}")] + internal sealed class GrammarBuilderPhrase : GrammarBuilderBase + { + #region Constructors + + internal GrammarBuilderPhrase(string phrase) + : this(phrase, false, SubsetMatchingMode.OrderedSubset) + { + } + + internal GrammarBuilderPhrase(string phrase, SubsetMatchingMode subsetMatchingCriteria) + : this(phrase, true, subsetMatchingCriteria) + { + } + + private GrammarBuilderPhrase(string phrase, bool subsetMatching, SubsetMatchingMode subsetMatchingCriteria) + { + _phrase = phrase; + _subsetMatching = subsetMatching; + switch (subsetMatchingCriteria) + { + case SubsetMatchingMode.OrderedSubset: + _matchMode = MatchMode.OrderedSubset; + break; + case SubsetMatchingMode.OrderedSubsetContentRequired: + _matchMode = MatchMode.OrderedSubsetContentRequired; + break; + case SubsetMatchingMode.Subsequence: + _matchMode = MatchMode.Subsequence; + break; + case SubsetMatchingMode.SubsequenceContentRequired: + _matchMode = MatchMode.SubsequenceContentRequired; + break; + } + } + + private GrammarBuilderPhrase(string phrase, bool subsetMatching, MatchMode matchMode) + { + _phrase = phrase; + _subsetMatching = subsetMatching; + _matchMode = matchMode; + } + + #endregion + + #region Public Methods + public override bool Equals(object obj) + { + GrammarBuilderPhrase refObj = obj as GrammarBuilderPhrase; + if (refObj == null) + { + return false; + } + return _phrase == refObj._phrase && _matchMode == refObj._matchMode && _subsetMatching == refObj._subsetMatching; + } + public override int GetHashCode() + { + return _phrase.GetHashCode(); + } + + #endregion + + #region Internal Methods + + internal override GrammarBuilderBase Clone() + { + return new GrammarBuilderPhrase(_phrase, _subsetMatching, _matchMode); + } + + internal override IElement CreateElement(IElementFactory elementFactory, IElement parent, IRule rule, IdentifierCollection ruleIds) + { + return CreatePhraseElement(elementFactory, parent); + } + + #endregion + + #region Internal Properties + + internal override string DebugSummary + { + get + { + return "'" + _phrase + "'"; + } + } + + #endregion + + #region Private Methods + + private IElement CreatePhraseElement(IElementFactory elementFactory, IElement parent) + { + if (_subsetMatching) + { + // Create and return the ISubset representing the current phrase + return elementFactory.CreateSubset(parent, _phrase, _matchMode); + } + else + { + if (elementFactory is SrgsElementCompilerFactory) + { + XmlParser.ParseText(parent, _phrase, null, null, -1f, new CreateTokenCallback(elementFactory.CreateToken)); + } + else + { + // Create and return the IElementText representing the current phrase + return elementFactory.CreateText(parent, _phrase); + } + } + return null; + } + + #endregion + + #region Private Fields + + private readonly string _phrase; + private readonly bool _subsetMatching; + private readonly MatchMode _matchMode; + + #endregion + } +} diff --git a/src/libraries/System.Speech/src/Internal/GrammarBuilding/GrammarBuilderRuleRef.cs b/src/libraries/System.Speech/src/Internal/GrammarBuilding/GrammarBuilderRuleRef.cs new file mode 100644 index 00000000000000..fafcf539f9ea0c --- /dev/null +++ b/src/libraries/System.Speech/src/Internal/GrammarBuilding/GrammarBuilderRuleRef.cs @@ -0,0 +1,75 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Speech.Internal.SrgsParser; + +namespace System.Speech.Internal.GrammarBuilding +{ + + internal sealed class GrammarBuilderRuleRef : GrammarBuilderBase + { + #region Constructors + + internal GrammarBuilderRuleRef(Uri uri, string rule) + { + _uri = uri.OriginalString + ((rule != null) ? "#" + rule : ""); + } + + private GrammarBuilderRuleRef(string sgrsUri) + { + _uri = sgrsUri; + } + + #endregion + + #region Public Methods + public override bool Equals(object obj) + { + GrammarBuilderRuleRef refObj = obj as GrammarBuilderRuleRef; + if (refObj == null) + { + return false; + } + return _uri == refObj._uri; + } + public override int GetHashCode() + { + return _uri.GetHashCode(); + } + + #endregion + + #region Internal Methods + + internal override GrammarBuilderBase Clone() + { + return new GrammarBuilderRuleRef(_uri); + } + + internal override IElement CreateElement(IElementFactory elementFactory, IElement parent, IRule rule, IdentifierCollection ruleIds) + { + Uri ruleUri = new(_uri, UriKind.RelativeOrAbsolute); + return elementFactory.CreateRuleRef(parent, ruleUri, null, null); + } + + #endregion + + #region Internal Properties + + internal override string DebugSummary + { + get + { + return "#" + _uri; + } + } + + #endregion + + #region Private Fields + + private readonly string _uri; + + #endregion + } +} diff --git a/src/libraries/System.Speech/src/Internal/GrammarBuilding/GrammarBuilderWildcard.cs b/src/libraries/System.Speech/src/Internal/GrammarBuilding/GrammarBuilderWildcard.cs new file mode 100644 index 00000000000000..8dcde18b5b14b5 --- /dev/null +++ b/src/libraries/System.Speech/src/Internal/GrammarBuilding/GrammarBuilderWildcard.cs @@ -0,0 +1,63 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Speech.Internal.SrgsParser; + +namespace System.Speech.Internal.GrammarBuilding +{ + + internal sealed class GrammarBuilderWildcard : GrammarBuilderBase + { + #region Constructors + + internal GrammarBuilderWildcard() + { + } + + #endregion + + #region Public Methods + public override bool Equals(object obj) + { + GrammarBuilderWildcard refObj = obj as GrammarBuilderWildcard; + return refObj != null; + } + public override int GetHashCode() + { + return base.GetHashCode(); + } + + #endregion + + #region Internal Methods + + internal override GrammarBuilderBase Clone() + { + return new GrammarBuilderWildcard(); + } + + internal override IElement CreateElement(IElementFactory elementFactory, IElement parent, IRule rule, IdentifierCollection ruleIds) + { + // Return a ruleref to Garbage + IRuleRef ruleRef = elementFactory.Garbage; + + elementFactory.InitSpecialRuleRef(parent, ruleRef); + + return ruleRef; + } + + #endregion + + #region Internal Properties + + internal override string DebugSummary + { + get + { + return "*"; + } + } + + #endregion + } +} diff --git a/src/libraries/System.Speech/src/Internal/GrammarBuilding/IdentifierCollection.cs b/src/libraries/System.Speech/src/Internal/GrammarBuilding/IdentifierCollection.cs new file mode 100644 index 00000000000000..8fe91e5bf8d243 --- /dev/null +++ b/src/libraries/System.Speech/src/Internal/GrammarBuilding/IdentifierCollection.cs @@ -0,0 +1,52 @@ +// 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; + +namespace System.Speech.Internal.GrammarBuilding +{ + + internal class IdentifierCollection + { + #region Constructors + + internal IdentifierCollection() + { + _identifiers = new List(); + CreateNewIdentifier("_"); + } + + #endregion + + #region Internal Methods + + internal string CreateNewIdentifier(string id) + { + if (!_identifiers.Contains(id)) + { + _identifiers.Add(id); + return id; + } + else + { + string newId; + int i = 1; + do + { + newId = id + i; + i++; + } while (_identifiers.Contains(newId)); + _identifiers.Add(newId); + return newId; + } + } + + #endregion + + #region Protected Fields + + protected List _identifiers; + + #endregion + } +} diff --git a/src/libraries/System.Speech/src/Internal/GrammarBuilding/ItemElement.cs b/src/libraries/System.Speech/src/Internal/GrammarBuilding/ItemElement.cs new file mode 100644 index 00000000000000..32fd275c0e1d3a --- /dev/null +++ b/src/libraries/System.Speech/src/Internal/GrammarBuilding/ItemElement.cs @@ -0,0 +1,107 @@ +// 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.Diagnostics; +using System.Speech.Internal.SrgsParser; +using System.Speech.Recognition; + +namespace System.Speech.Internal.GrammarBuilding +{ + [DebuggerDisplay("{DebugSummary}")] + internal sealed class ItemElement : BuilderElements + { + #region Constructors + + internal ItemElement(GrammarBuilderBase builder) + : this(builder, 1, 1) + { + } + + internal ItemElement(int minRepeat, int maxRepeat) + : this((GrammarBuilderBase)null, minRepeat, maxRepeat) + { + } + + internal ItemElement(GrammarBuilderBase builder, int minRepeat, int maxRepeat) + { + if (builder != null) + { + Add(builder); + } + _minRepeat = minRepeat; + _maxRepeat = maxRepeat; + } + + internal ItemElement(List builders, int minRepeat, int maxRepeat) + { + foreach (GrammarBuilderBase builder in builders) + { + Items.Add(builder); + } + _minRepeat = minRepeat; + _maxRepeat = maxRepeat; + } + + internal ItemElement(GrammarBuilder builders) + { + foreach (GrammarBuilderBase builder in builders.InternalBuilder.Items) + { + Items.Add(builder); + } + } + + #endregion + + #region Public Methods + public override bool Equals(object obj) + { + ItemElement refObj = obj as ItemElement; + if (refObj == null) + { + return false; + } + if (!base.Equals(obj)) + { + return false; + } + return _minRepeat == refObj._minRepeat && _maxRepeat == refObj._maxRepeat; + } + public override int GetHashCode() + { + return base.GetHashCode(); + } + + #endregion + + #region Internal Methods + + internal override GrammarBuilderBase Clone() + { + ItemElement item = new(_minRepeat, _maxRepeat); + item.CloneItems(this); + return item; + } + + internal override IElement CreateElement(IElementFactory elementFactory, IElement parent, IRule rule, IdentifierCollection ruleIds) + { + // Create and return the real item (the item including the grammar) + // for the current grammar + IItem item = elementFactory.CreateItem(parent, rule, _minRepeat, _maxRepeat, 0.5f, 1f); + + // Create the children elements + CreateChildrenElements(elementFactory, item, rule, ruleIds); + + return item; + } + + #endregion + + #region Private Fields + + private readonly int _minRepeat = 1; + private readonly int _maxRepeat = 1; + + #endregion + } +} diff --git a/src/libraries/System.Speech/src/Internal/GrammarBuilding/OneOfElement.cs b/src/libraries/System.Speech/src/Internal/GrammarBuilding/OneOfElement.cs new file mode 100644 index 00000000000000..8045b2e59e8aca --- /dev/null +++ b/src/libraries/System.Speech/src/Internal/GrammarBuilding/OneOfElement.cs @@ -0,0 +1,73 @@ +// 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; +using System.Speech.Internal.SrgsParser; +using System.Text; + +namespace System.Speech.Internal.GrammarBuilding +{ + [DebuggerDisplay("{DebugSummary}")] + internal sealed class OneOfElement : BuilderElements + { + #region Constructors + + internal OneOfElement() + { + } + + #endregion + + #region Internal Methods + + internal override GrammarBuilderBase Clone() + { + OneOfElement oneOf = new(); + oneOf.CloneItems(this); + return oneOf; + } + + internal override IElement CreateElement(IElementFactory elementFactory, IElement parent, IRule rule, IdentifierCollection ruleIds) + { + // Create and return the IOneOf representing the current object + IOneOf oneOf = elementFactory.CreateOneOf(parent, rule); + foreach (GrammarBuilderBase item in Items) + { + ItemElement newItem = item as ItemElement; + if (newItem == null) + { + newItem = new ItemElement(item); + } + + IItem element = (IItem)newItem.CreateElement(elementFactory, oneOf, rule, ruleIds); + element.PostParse(oneOf); + elementFactory.AddItem(oneOf, element); + } + return oneOf; + } + + #endregion + + #region Internal Properties + + internal override string DebugSummary + { + get + { + StringBuilder sb = new(); + + foreach (GrammarBuilderBase item in Items) + { + if (sb.Length > 0) + { + sb.Append(','); + } + sb.Append(item.DebugSummary); + } + return "[" + sb.ToString() + "]"; + } + } + + #endregion + } +} diff --git a/src/libraries/System.Speech/src/Internal/GrammarBuilding/RuleElement.cs b/src/libraries/System.Speech/src/Internal/GrammarBuilding/RuleElement.cs new file mode 100644 index 00000000000000..7fd7699e9aeb6e --- /dev/null +++ b/src/libraries/System.Speech/src/Internal/GrammarBuilding/RuleElement.cs @@ -0,0 +1,121 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Speech.Internal.SrgsParser; + +namespace System.Speech.Internal.GrammarBuilding +{ + + internal sealed class RuleElement : BuilderElements + { + #region Constructors + + internal RuleElement(string name) + { + _name = name; + } + + internal RuleElement(GrammarBuilderBase builder, string name) + : this(name) + { + Add(builder); + } + + #endregion + + #region Public Methods + public override bool Equals(object obj) + { + RuleElement refObj = obj as RuleElement; + if (refObj == null) + { + return false; + } + if (!base.Equals(obj)) + { + return false; + } + return _name == refObj._name; + } + + public override int GetHashCode() + { + return base.GetHashCode(); + } + + #endregion + + #region Internal Methods + + internal override GrammarBuilderBase Clone() + { + RuleElement rule = new(_name); + rule.CloneItems(this); + return rule; + } + + internal override IElement CreateElement(IElementFactory elementFactory, IElement parent, IRule rule, IdentifierCollection ruleIds) + { + if (_rule == null) + { + IGrammar grammar = elementFactory.Grammar; + + // Create the rule + _ruleName = ruleIds.CreateNewIdentifier(Name); + + _rule = grammar.CreateRule(_ruleName, RulePublic.False, RuleDynamic.NotSet, false); + + // Create the children elements + CreateChildrenElements(elementFactory, _rule, ruleIds); + + _rule.PostParse(grammar); + } + return _rule; + } + + internal override int CalcCount(BuilderElements parent) + { + // clear any existing value + _rule = null; + return base.CalcCount(parent); + } + + #endregion + + #region Internal Properties + + internal override string DebugSummary + { + get + { + return _name + "=" + base.DebugSummary; + } + } + + internal string Name + { + get + { + return _name; + } + } + + internal string RuleName + { + get + { + return _ruleName; + } + } + + #endregion + + #region Private Fields + + private readonly string _name; + private string _ruleName; + private IRule _rule; + + #endregion + } +} diff --git a/src/libraries/System.Speech/src/Internal/GrammarBuilding/RuleRefElement.cs b/src/libraries/System.Speech/src/Internal/GrammarBuilding/RuleRefElement.cs new file mode 100644 index 00000000000000..532c05501bf412 --- /dev/null +++ b/src/libraries/System.Speech/src/Internal/GrammarBuilding/RuleRefElement.cs @@ -0,0 +1,96 @@ +// 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; +using System.Speech.Internal.SrgsParser; + +namespace System.Speech.Internal.GrammarBuilding +{ + [DebuggerDisplay("{DebugSummary}")] + internal sealed class RuleRefElement : GrammarBuilderBase + { + #region Constructors + + internal RuleRefElement(RuleElement rule) + { + _rule = rule; + } + + internal RuleRefElement(RuleElement rule, string semanticKey) + { + _rule = rule; + _semanticKey = semanticKey; + } + + #endregion + + #region Public Methods + public override bool Equals(object obj) + { + RuleRefElement refObj = obj as RuleRefElement; + if (refObj == null) + { + return false; + } + return _semanticKey == refObj._semanticKey && _rule.Equals(refObj._rule); + } + public override int GetHashCode() + { + return base.GetHashCode(); + } + + #endregion + + #region Internal Methods + + internal void Add(GrammarBuilderBase item) + { + _rule.Add(item); + } + + internal override GrammarBuilderBase Clone() + { + return new RuleRefElement(_rule, _semanticKey); + } + + internal void CloneItems(RuleRefElement builders) + { + _rule.CloneItems(builders._rule); + } + + internal override IElement CreateElement(IElementFactory elementFactory, IElement parent, IRule rule, IdentifierCollection ruleIds) + { + // Create the new rule and add the reference to the item + return elementFactory.CreateRuleRef(parent, new Uri("#" + Rule.RuleName, UriKind.Relative), _semanticKey, null); + } + + #endregion + + #region Internal Properties + + internal RuleElement Rule + { + get + { + return _rule; + } + } + + internal override string DebugSummary + { + get + { + return "#" + Rule.Name + (_semanticKey != null ? ":" + _semanticKey : ""); + } + } + + #endregion + + #region Private Fields + + private readonly RuleElement _rule; + private readonly string _semanticKey; + + #endregion + } +} diff --git a/src/libraries/System.Speech/src/Internal/GrammarBuilding/SemanticKeyElement.cs b/src/libraries/System.Speech/src/Internal/GrammarBuilding/SemanticKeyElement.cs new file mode 100644 index 00000000000000..5e94f17d96a433 --- /dev/null +++ b/src/libraries/System.Speech/src/Internal/GrammarBuilding/SemanticKeyElement.cs @@ -0,0 +1,102 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Speech.Internal.SrgsParser; +using System.Speech.Recognition; + +namespace System.Speech.Internal.GrammarBuilding +{ + + internal sealed class SemanticKeyElement : BuilderElements + { + #region Constructors + + internal SemanticKeyElement(string semanticKey) + { + _semanticKey = semanticKey; + RuleElement rule = new(semanticKey); + _ruleRef = new RuleRefElement(rule, _semanticKey); + Items.Add(rule); + Items.Add(_ruleRef); + } + + #endregion + + #region Public Methods + public override bool Equals(object obj) + { + SemanticKeyElement refObj = obj as SemanticKeyElement; + if (refObj == null) + { + return false; + } + if (!base.Equals(obj)) + { + return false; + } + // No need to check for the equality on _ruleRef. The children are in the Items, not the underlying rule + return _semanticKey == refObj._semanticKey; + } + + public override int GetHashCode() + { + return base.GetHashCode(); + } + + #endregion + + #region Internal Methods + + internal new void Add(string phrase) + { + _ruleRef.Add(new GrammarBuilderPhrase(phrase)); + } + + internal new void Add(GrammarBuilder builder) + { + foreach (GrammarBuilderBase item in builder.InternalBuilder.Items) + { + _ruleRef.Add(item); + } + } + + internal override GrammarBuilderBase Clone() + { + SemanticKeyElement semanticKeyElement = new(_semanticKey); + semanticKeyElement._ruleRef.CloneItems(_ruleRef); + return semanticKeyElement; + } + + internal override IElement CreateElement(IElementFactory elementFactory, IElement parent, IRule rule, IdentifierCollection ruleIds) + { + // Create the rule associated with this key + _ruleRef.Rule.CreateElement(elementFactory, parent, rule, ruleIds); + + // Create the ruleRef + IElement ruleRef = _ruleRef.CreateElement(elementFactory, parent, rule, ruleIds); + + return ruleRef; + } + + #endregion + + #region Internal Properties + + internal override string DebugSummary + { + get + { + return _ruleRef.Rule.DebugSummary; + } + } + + #endregion + + #region Private Fields + + private readonly string _semanticKey; + private readonly RuleRefElement _ruleRef; + + #endregion + } +} diff --git a/src/libraries/System.Speech/src/Internal/GrammarBuilding/TagElement.cs b/src/libraries/System.Speech/src/Internal/GrammarBuilding/TagElement.cs new file mode 100644 index 00000000000000..1f199e608b1995 --- /dev/null +++ b/src/libraries/System.Speech/src/Internal/GrammarBuilding/TagElement.cs @@ -0,0 +1,111 @@ +// 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; +using System.Speech.Internal.SrgsParser; +using System.Speech.Recognition; + +namespace System.Speech.Internal.GrammarBuilding +{ + [DebuggerDisplay("{DebugSummary}")] + internal sealed class TagElement : BuilderElements + { + #region Constructors + + internal TagElement(object value) + { + _value = value; + } + + internal TagElement(GrammarBuilderBase builder, object value) + : this(value) + { + Add(builder); + } + + internal TagElement(GrammarBuilder builder, object value) + : this(value) + { + Add(builder); + } + + #endregion + + #region Public Methods + public override bool Equals(object obj) + { + TagElement refObj = obj as TagElement; + if (refObj == null) + { + return false; + } + if (!base.Equals(obj)) + { + return false; + } + return _value.Equals(refObj._value); + } + + public override int GetHashCode() + { + return base.GetHashCode(); + } + + #endregion + + #region Internal Methods + + internal override GrammarBuilderBase Clone() + { + TagElement tag = new(_value); + tag.CloneItems(this); + return tag; + } + + internal override IElement CreateElement(IElementFactory elementFactory, IElement parent, IRule rule, IdentifierCollection ruleIds) + { + // Create the children elements + IItem item = parent as IItem; + if (item != null) + { + CreateChildrenElements(elementFactory, item, rule, ruleIds); + } + else + { + if (parent == rule) + { + CreateChildrenElements(elementFactory, rule, ruleIds); + } + else + { + System.Diagnostics.Debug.Assert(false); + } + } + + // Create the tag element at the end only if there were some children + IPropertyTag tag = elementFactory.CreatePropertyTag(parent); + tag.NameValue(parent, null, _value); + return tag; + } + + #endregion + + #region Internal Properties + + internal override string DebugSummary + { + get + { + return base.DebugSummary + " {" + _value + "}"; + } + } + + #endregion + + #region Private Fields + + private readonly object _value; + + #endregion + } +} diff --git a/src/libraries/System.Speech/src/Internal/HGlobalSafeHandle.cs b/src/libraries/System.Speech/src/Internal/HGlobalSafeHandle.cs new file mode 100644 index 00000000000000..d7a0ddbd3ce11b --- /dev/null +++ b/src/libraries/System.Speech/src/Internal/HGlobalSafeHandle.cs @@ -0,0 +1,101 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Runtime.InteropServices; + +namespace System.Speech.Internal +{ + /// + /// Encapsulate SafeHandle for Win32 Memory Handles + /// + internal sealed class HGlobalSafeHandle : SafeHandle + { + #region Constructors + + internal HGlobalSafeHandle() : base(IntPtr.Zero, true) + { + } + + // This destructor will run only if the Dispose method + // does not get called. + ~HGlobalSafeHandle() + { + Dispose(false); + } + + protected override void Dispose(bool disposing) + { + ReleaseHandle(); + base.Dispose(disposing); + } + + #endregion + + #region internal Methods + + internal IntPtr Buffer(int size) + { + if (size > _bufferSize) + { + if (_bufferSize == 0) + { + SetHandle(Marshal.AllocHGlobal(size)); + } + else + { + SetHandle(Marshal.ReAllocHGlobal(handle, (IntPtr)size)); + } + + GC.AddMemoryPressure(size - _bufferSize); + _bufferSize = size; + } + + return handle; + } + + /// + /// True if the no memory is allocated + /// + public override bool IsInvalid + { + get + { + return handle == IntPtr.Zero; + } + } + + #endregion + + #region Protected Methods + + /// + /// Releases the Win32 Memory handle + /// + protected override bool ReleaseHandle() + { + if (handle != IntPtr.Zero) + { + // Reset the extra information given to the GC + if (_bufferSize > 0) + { + GC.RemoveMemoryPressure(_bufferSize); + _bufferSize = 0; + } + + Marshal.FreeHGlobal(handle); + handle = IntPtr.Zero; + return true; + } + + return false; + } + + #endregion + + #region Private Fields + + private int _bufferSize; + + #endregion + } +} diff --git a/src/libraries/System.Speech/src/Internal/Helpers.cs b/src/libraries/System.Speech/src/Internal/Helpers.cs new file mode 100644 index 00000000000000..85b567434faca4 --- /dev/null +++ b/src/libraries/System.Speech/src/Internal/Helpers.cs @@ -0,0 +1,115 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Globalization; +using System.IO; + +namespace System.Speech.Internal +{ + internal static class Helpers + { + #region Internal Methods + + // Disable parameter validation check + + // Throws exception if the specified Rule does not have a valid Id. + internal static void ThrowIfEmptyOrNull(string s, string paramName) + { + if (string.IsNullOrEmpty(s)) + { + if (s == null) + { + throw new ArgumentNullException(paramName); + } + else + { + throw new ArgumentException(SR.Get(SRID.StringCanNotBeEmpty, paramName), paramName); + } + } + } + + // Throws exception if the specified Rule does not have a valid Id. + internal static void ThrowIfNull(object value, string paramName) + { + if (value == null) + { + throw new ArgumentNullException(paramName); + } + } + + internal static bool CompareInvariantCulture(CultureInfo culture1, CultureInfo culture2) + { + // If perfect match easy + if (culture1.Equals(culture2)) + { + return true; + } + + // Compare the Neutral culture + while (!culture1.IsNeutralCulture) + { + culture1 = culture1.Parent; + } + while (!culture2.IsNeutralCulture) + { + culture2 = culture2.Parent; + } + return culture1.Equals(culture2); + } + + // Copy the input cfg to the output. + // Streams point to the start of the data on entry and to the end on exit + internal static void CopyStream(Stream inputStream, Stream outputStream, int bytesToCopy) + { + // Copy using an intermediate buffer of a reasonable size. + int bufferSize = bytesToCopy > 4096 ? 4096 : bytesToCopy; + byte[] buffer = new byte[bufferSize]; + int bytesRead; + while (bytesToCopy > 0) + { + bytesRead = inputStream.Read(buffer, 0, bufferSize); + if (bytesRead <= 0) + { + throw new EndOfStreamException(SR.Get(SRID.StreamEndedUnexpectedly)); + } + outputStream.Write(buffer, 0, bytesRead); + bytesToCopy -= bytesRead; + } + } + + // Copy the input cfg to the output. + // inputStream points to the start of the data on entry and to the end on exit + internal static byte[] ReadStreamToByteArray(Stream inputStream, int bytesToCopy) + { + byte[] outputArray = new byte[bytesToCopy]; + BlockingRead(inputStream, outputArray, 0, bytesToCopy); + return outputArray; + } + + internal static void BlockingRead(Stream stream, byte[] buffer, int offset, int count) + { + // Stream is not like IStream - it will block until some data is available but not necessarily all of it. + while (count > 0) + { + int read = stream.Read(buffer, offset, count); + if (read <= 0) // End of stream + { + throw new EndOfStreamException(); + } + count -= read; + offset += read; + } + } + + #endregion + + #region Internal fields + + internal static readonly char[] _achTrimChars = new char[] { ' ', '\t', '\n', '\r' }; + + // Size of a char (avoid to use the marshal class + internal const int _sizeOfChar = 2; + + #endregion + } +} diff --git a/src/libraries/System.Speech/src/Internal/ObjectToken/ObjectToken.cs b/src/libraries/System.Speech/src/Internal/ObjectToken/ObjectToken.cs new file mode 100644 index 00000000000000..1d38e938bbdd77 --- /dev/null +++ b/src/libraries/System.Speech/src/Internal/ObjectToken/ObjectToken.cs @@ -0,0 +1,334 @@ +// 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; +using System.Globalization; +using System.IO; +using System.Reflection; +using System.Runtime.InteropServices; +using System.Speech.Internal.SapiInterop; + +namespace System.Speech.Internal.ObjectTokens +{ + [DebuggerDisplay("{Name}")] + internal class ObjectToken : RegistryDataKey, ISpObjectToken + { + #region Constructors + + protected ObjectToken(ISpObjectToken sapiObjectToken, bool disposeSapiToken) + : base(sapiObjectToken) + { + if (sapiObjectToken == null) + { + throw new ArgumentNullException(nameof(sapiObjectToken)); + } + + _sapiObjectToken = sapiObjectToken; + _disposeSapiObjectToken = disposeSapiToken; + } + + /// + /// Creates a ObjectToken from an already-existing ISpObjectToken. + /// Assumes the token was created through enumeration, thus should not be disposed by us. + /// + /// ObjectToken object + internal static ObjectToken Open(ISpObjectToken sapiObjectToken) + { + return new ObjectToken(sapiObjectToken, false); + } + + /// + /// Creates a new ObjectToken from a category + /// Unlike the other Open overload, this one creates a new SAPI object, so Dispose must be called if + /// you are creating ObjectTokens with this function. + /// + /// ObjectToken object + internal static ObjectToken Open(string sCategoryId, string sTokenId, bool fCreateIfNotExist) + { + ISpObjectToken sapiObjectToken = (ISpObjectToken)new SpObjectToken(); + + try + { + sapiObjectToken.SetId(sCategoryId, sTokenId, fCreateIfNotExist); + } + catch (Exception) + { + Marshal.ReleaseComObject(sapiObjectToken); + return null; + } + + return new ObjectToken(sapiObjectToken, true); + } + + protected override void Dispose(bool disposing) + { + try + { + if (disposing) + { + if (_disposeSapiObjectToken == true && _sapiObjectToken != null) + { + Marshal.ReleaseComObject(_sapiObjectToken); + _sapiObjectToken = null; + } + if (_attributes != null) + { + _attributes.Dispose(); + _attributes = null; + } + } + } + finally + { + base.Dispose(disposing); + } + } + + #endregion + + #region public Methods + + /// + /// Tests whether two AutomationIdentifier objects are equivalent + /// + public override bool Equals(object obj) + { + ObjectToken token = obj as ObjectToken; + return token != null && string.Compare(Id, token.Id, StringComparison.OrdinalIgnoreCase) == 0; + } + + /// + /// Overrides Object.GetHashCode() + /// + public override int GetHashCode() + { + return Id.GetHashCode(); + } + + #endregion + + #region Internal Properties + + internal RegistryDataKey Attributes + { + get + { + return _attributes != null ? _attributes : (_attributes = OpenKey("Attributes")); + } + } + + internal ISpObjectToken SAPIToken + { + get + { + return _sapiObjectToken; + } + } + + /// + /// Returns the Age from a voice token + /// + internal string Age + { + get + { + string age; + if (Attributes == null || !Attributes.TryGetString("Age", out age)) + { + age = string.Empty; + } + return age; + } + } + + /// + /// Returns the gender + /// + internal string Gender + { + get + { + string gender; + if (Attributes == null || !Attributes.TryGetString("Gender", out gender)) + { + gender = string.Empty; + } + return gender; + } + } + + /// + /// Returns the Name for the voice + /// Look first in the Name attribute, if not available then get the default string + /// + internal string TokenName() + { + string name = string.Empty; + if (Attributes != null) + { + Attributes.TryGetString("Name", out name); + + if (string.IsNullOrEmpty(name)) + { + TryGetString(null, out name); + } + } + return name; + } + + /// + /// Returns the Culture defined in the Language field for a token + /// + internal CultureInfo Culture + { + get + { + CultureInfo culture = null; + string langId; + if (Attributes.TryGetString("Language", out langId)) + { + culture = SapiAttributeParser.GetCultureInfoFromLanguageString(langId); + } + return culture; + } + } + + /// + /// Returns the Culture defined in the Language field for a token + /// + internal string Description + { + get + { + string description = string.Empty; + string sCultureId = string.Format(CultureInfo.InvariantCulture, "{0:x}", CultureInfo.CurrentUICulture.LCID); + if (!TryGetString(sCultureId, out description)) + { + TryGetString(null, out description); + } + return description; + } + } + + #endregion + + #region internal Methods + + #region ISpObjectToken Implementation + + public void SetId([MarshalAs(UnmanagedType.LPWStr)] string pszCategoryId, [MarshalAs(UnmanagedType.LPWStr)] string pszTokenId, [MarshalAs(UnmanagedType.Bool)] bool fCreateIfNotExist) + { + throw new NotImplementedException(); + } + + public void GetId([MarshalAs(UnmanagedType.LPWStr)] out IntPtr ppszCoMemTokenId) + { + ppszCoMemTokenId = Marshal.StringToCoTaskMemUni(Id); + } + + public void Slot15() { throw new NotImplementedException(); } // void GetCategory(out ISpObjectTokenCategory ppTokenCategory); + public void Slot16() { throw new NotImplementedException(); } // void CreateInstance(object pUnkOuter, UInt32 dwClsContext, ref Guid riid, ref IntPtr ppvObject); + public void Slot17() { throw new NotImplementedException(); } // void GetStorageFileName(ref Guid clsidCaller, [MarshalAs(UnmanagedType.LPWStr)] string pszValueName, [MarshalAs(UnmanagedType.LPWStr)] string pszFileNameSpecifier, UInt32 nFolder, [MarshalAs(UnmanagedType.LPWStr)] out string ppszFilePath); + public void Slot18() { throw new NotImplementedException(); } // void RemoveStorageFileName(ref Guid clsidCaller, [MarshalAs(UnmanagedType.LPWStr)] string pszKeyName, int fDeleteFile); + public void Slot19() { throw new NotImplementedException(); } // void Remove(ref Guid pclsidCaller); + public void Slot20() { throw new NotImplementedException(); } // void IsUISupported([MarshalAs(UnmanagedType.LPWStr)] string pszTypeOfUI, IntPtr pvExtraData, UInt32 cbExtraData, object punkObject, ref Int32 pfSupported); + public void Slot21() { throw new NotImplementedException(); } // void DisplayUI(UInt32 hWndParent, [MarshalAs(UnmanagedType.LPWStr)] string pszTitle, [MarshalAs(UnmanagedType.LPWStr)] string pszTypeOfUI, IntPtr pvExtraData, UInt32 cbExtraData, object punkObject); + public void MatchesAttributes([MarshalAs(UnmanagedType.LPWStr)] string pszAttributes, [MarshalAs(UnmanagedType.Bool)] out bool pfMatches) { throw new NotImplementedException(); } + + #endregion + + /// + /// Check if the token supports the attributes list given in. The + /// attributes list has the same format as the required attributes given to + /// SpEnumTokens. + /// + internal bool MatchesAttributes(string[] sAttributes) + { + bool fMatch = true; + + for (int iAttribute = 0; iAttribute < sAttributes.Length; iAttribute++) + { + string s = sAttributes[iAttribute]; + fMatch &= HasValue(s) || (Attributes != null && Attributes.HasValue(s)); + if (!fMatch) + { + break; + } + } + return fMatch; + } + + internal T CreateObjectFromToken(string name) + { + T instanceValue = default(T); + string clsid; + + if (!TryGetString(name, out clsid)) + { + throw new ArgumentException(SR.Get(SRID.TokenCannotCreateInstance)); + } + + try + { + // Application Class Id + Type type = Type.GetTypeFromCLSID(new Guid(clsid)); + + // Create the object instance + instanceValue = (T)Activator.CreateInstance(type); + + // Initialize the instance + ISpObjectWithToken objectWithToken = instanceValue as ISpObjectWithToken; + if (objectWithToken != null) + { + int hresult = objectWithToken.SetObjectToken(this); + if (hresult < 0) + { + throw new ArgumentException(SR.Get(SRID.TokenCannotCreateInstance)); + } + } + else + { + Debug.Fail("Cannot query for interface " + typeof(ISpObjectWithToken).GUID + " from COM class " + clsid); + } + } + catch (Exception e) + { + if (e is MissingMethodException || e is TypeLoadException || e is FileLoadException || e is FileNotFoundException || e is MethodAccessException || e is MemberAccessException || e is TargetInvocationException || e is InvalidComObjectException || e is NotSupportedException || e is FormatException) + { + throw new ArgumentException(SR.Get(SRID.TokenCannotCreateInstance)); + } + throw; + } + return instanceValue; + } + + #endregion + + #region private Methods + + #endregion + + #region Private Types + + //--- ISpObjectWithToken ---------------------------------------------------- + [ComImport, Guid("5B559F40-E952-11D2-BB91-00C04F8EE6C0"), InterfaceType(ComInterfaceType.InterfaceIsIUnknown)] + private interface ISpObjectWithToken + { + [PreserveSig] + int SetObjectToken(ISpObjectToken pToken); + [PreserveSig] + int GetObjectToken(IntPtr ppToken); + } + + #endregion + #region private Fields + + private ISpObjectToken _sapiObjectToken; + + private bool _disposeSapiObjectToken; + + private RegistryDataKey _attributes; + + #endregion + } +} diff --git a/src/libraries/System.Speech/src/Internal/ObjectToken/ObjectTokenCategory.cs b/src/libraries/System.Speech/src/Internal/ObjectToken/ObjectTokenCategory.cs new file mode 100644 index 00000000000000..e78160f17e78e7 --- /dev/null +++ b/src/libraries/System.Speech/src/Internal/ObjectToken/ObjectTokenCategory.cs @@ -0,0 +1,103 @@ +// 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; +using System.Collections.Generic; +using System.Globalization; +using System.Runtime.InteropServices; +using System.Speech.Internal.SapiInterop; + +namespace System.Speech.Internal.ObjectTokens +{ + internal class ObjectTokenCategory : RegistryDataKey, IEnumerable + { + #region Constructors + + protected ObjectTokenCategory(string keyId, RegistryDataKey key) + : base(keyId, key) + { + } + + internal static ObjectTokenCategory Create(string sCategoryId) + { + RegistryDataKey key = RegistryDataKey.Open(sCategoryId, true); + return new ObjectTokenCategory(sCategoryId, key); + } + + #endregion + + #region internal Methods + + internal ObjectToken OpenToken(string keyName) + { + // Check if the token is for a voice + string tokenName = keyName; + if (!string.IsNullOrEmpty(tokenName) && tokenName.IndexOf("HKEY_", StringComparison.Ordinal) != 0) + { + tokenName = string.Format(CultureInfo.InvariantCulture, @"{0}\Tokens\{1}", Id, tokenName); + } + + return ObjectToken.Open(null, tokenName, false); + } + + internal IList FindMatchingTokens(string requiredAttributes, string optionalAttributes) + { + IList objectTokenList = new List(); + ISpObjectTokenCategory category = null; + IEnumSpObjectTokens enumTokens = null; + + try + { + // Note - enumerated tokens should not be torn down/disposed by us (see SpInitTokenList in spuihelp.h) + category = (ISpObjectTokenCategory)new SpObjectTokenCategory(); + category.SetId(_sKeyId, false); + category.EnumTokens(requiredAttributes, optionalAttributes, out enumTokens); + + uint tokenCount; + enumTokens.GetCount(out tokenCount); + for (uint index = 0; index < tokenCount; ++index) + { + ISpObjectToken spObjectToken = null; + + enumTokens.Item(index, out spObjectToken); + ObjectToken objectToken = ObjectToken.Open(spObjectToken); + objectTokenList.Add(objectToken); + } + } + finally + { + if (enumTokens != null) + { + Marshal.ReleaseComObject(enumTokens); + } + if (category != null) + { + Marshal.ReleaseComObject(category); + } + } + + return objectTokenList; + } + + #region IEnumerable implementation + + IEnumerator IEnumerable.GetEnumerator() + { + IList objectTokenList = FindMatchingTokens(null, null); + + foreach (ObjectToken objectToken in objectTokenList) + { + yield return objectToken; + } + } + + IEnumerator IEnumerable.GetEnumerator() + { + return ((IEnumerable)this).GetEnumerator(); + } + + #endregion + + #endregion + } +} diff --git a/src/libraries/System.Speech/src/Internal/ObjectToken/RegistryDataKey.cs b/src/libraries/System.Speech/src/Internal/ObjectToken/RegistryDataKey.cs new file mode 100644 index 00000000000000..0f58c5b99dbb0b --- /dev/null +++ b/src/libraries/System.Speech/src/Internal/ObjectToken/RegistryDataKey.cs @@ -0,0 +1,539 @@ +// 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; +using System.Collections.Generic; +using System.Diagnostics; +using System.Runtime.InteropServices; +using System.Speech.Internal.SapiInterop; +using Microsoft.Win32; +using Microsoft.Win32.SafeHandles; + +namespace System.Speech.Internal.ObjectTokens +{ + [DebuggerDisplay("{Name}")] + internal class RegistryDataKey : ISpDataKey, IEnumerable, IDisposable + { + #region Constructors + + protected RegistryDataKey(string fullPath, SafeRegistryHandle regHandle) + { + ISpRegDataKey regKey = (ISpRegDataKey)new SpDataKey(); + SAPIErrorCodes hresult = (SAPIErrorCodes)regKey.SetKey(regHandle, false); + regHandle?.Close(); + if ((hresult != SAPIErrorCodes.S_OK) && (hresult != SAPIErrorCodes.SPERR_ALREADY_INITIALIZED)) + { + throw new InvalidOperationException(); + } + + _sapiRegKey = regKey; + _sKeyId = fullPath; + _disposeSapiKey = true; + } + + protected RegistryDataKey(string fullPath, RegistryKey managedRegKey) : + this(fullPath, managedRegKey.Handle) + { + } + + protected RegistryDataKey(string fullPath, RegistryDataKey copyKey) + { + this._sKeyId = fullPath; + this._sapiRegKey = copyKey._sapiRegKey; + this._disposeSapiKey = copyKey._disposeSapiKey; + } + + protected RegistryDataKey(string fullPath, ISpDataKey copyKey, bool shouldDispose) + { + this._sKeyId = fullPath; + this._sapiRegKey = copyKey; + this._disposeSapiKey = shouldDispose; + } + + protected RegistryDataKey(ISpObjectToken sapiToken) : + this(GetTokenIdFromToken(sapiToken), sapiToken, false) + { + } + + internal static RegistryDataKey Open(string registryPath, bool fCreateIfNotExist) + { + // Sanity check + if (string.IsNullOrEmpty(registryPath)) + { + return null; + } + + // If the last character is a '\', get rid of it + registryPath = registryPath.Trim(new char[] { '\\' }); + + string rootPath = GetFirstKeyAndParseRemainder(ref registryPath); + + // Get the native registry handle and subkey path + SafeRegistryHandle regHandle = RootHKEYFromRegPath(rootPath); + + // If there's no root, we can't do anything. + if (regHandle == null || regHandle.IsInvalid) + { + return null; + } + + RegistryDataKey rootKey = new(rootPath, regHandle); + + // If the path was only a root, we can directly return the key; otherwise, + // we need to open a subkey and return that. + if (string.IsNullOrEmpty(registryPath)) + { + return rootKey; + } + else + { + RegistryDataKey subKey = OpenSubKey(rootKey, registryPath, fCreateIfNotExist); + return subKey; + } + } + + internal static RegistryDataKey Create(string keyId, RegistryKey hkey) + { + return new RegistryDataKey(keyId, hkey); + } + + private static RegistryDataKey OpenSubKey(RegistryDataKey baseKey, string registryPath, bool createIfNotExist) + { + if (string.IsNullOrEmpty(registryPath) || null == baseKey) + { + return null; + } + + string nextKeyPath = GetFirstKeyAndParseRemainder(ref registryPath); + + RegistryDataKey nextKey = createIfNotExist ? baseKey.CreateKey(nextKeyPath) : baseKey.OpenKey(nextKeyPath); + + if (string.IsNullOrEmpty(registryPath)) + { + return nextKey; + } + else + { + RegistryDataKey recursiveKey = OpenSubKey(nextKey, registryPath, createIfNotExist); + return recursiveKey; + } + } + + private static string GetTokenIdFromToken(ISpObjectToken sapiToken) + { + IntPtr sapiTokenId = IntPtr.Zero; + string tokenId; + + try + { + sapiToken.GetId(out sapiTokenId); + tokenId = Marshal.PtrToStringUni(sapiTokenId); + } + finally + { + Marshal.FreeCoTaskMem(sapiTokenId); + } + + return tokenId; + } + + /// + /// Needed by IEnumerable + /// + public void Dispose() + { + Dispose(true); + GC.SuppressFinalize(this); + } + + #endregion + + #region internal Methods + + #region ISpDataKey Implementation + + // ISpDataKey Methods + + /// + /// Writes the specified binary data to the registry. + /// + [PreserveSig] + public int SetData( + [MarshalAs(UnmanagedType.LPWStr)] string valueName, + [MarshalAs(UnmanagedType.SysUInt)] uint cbData, + [MarshalAs(UnmanagedType.LPArray, SizeParamIndex = 1)] byte[] data) + { + return _sapiRegKey.SetData(valueName, cbData, data); + } + + /// + /// Reads the specified binary data from the registry. + /// + [PreserveSig] + public int GetData( + [MarshalAs(UnmanagedType.LPWStr)] string valueName, + [MarshalAs(UnmanagedType.SysUInt)] ref uint pcbData, + [MarshalAs(UnmanagedType.LPArray, SizeParamIndex = 1), Out] byte[] data) + { + return _sapiRegKey.GetData(valueName, ref pcbData, data); + } + + /// + /// Writes the specified string value from the registry. If valueName + /// is NULL then the default value of the registry key is read. + /// + [PreserveSig] + public int SetStringValue( + [MarshalAs(UnmanagedType.LPWStr)] string valueName, + [MarshalAs(UnmanagedType.LPWStr)] string value) + { + return _sapiRegKey.SetStringValue(valueName, value); + } + + /// + /// Reads the specified string value to the registry. If valueName is + /// NULL then the default value of the registry key is read. + /// + [PreserveSig] + public int GetStringValue( + [MarshalAs(UnmanagedType.LPWStr)] string valueName, + [MarshalAs(UnmanagedType.LPWStr)] out string value) + { + return _sapiRegKey.GetStringValue(valueName, out value); + } + + /// + /// Writes the specified DWORD to the registry. + /// + [PreserveSig] + public int SetDWORD( + [MarshalAs(UnmanagedType.LPWStr)] string valueName, + [MarshalAs(UnmanagedType.SysUInt)] uint value) + { + return _sapiRegKey.SetDWORD(valueName, value); + } + + /// + /// Reads the specified DWORD from the registry. + /// + [PreserveSig] + public int GetDWORD([MarshalAs(UnmanagedType.LPWStr)] string valueName, ref uint pdwValue) + { + return _sapiRegKey.GetDWORD(valueName, ref pdwValue); + } + + /// + /// Opens a sub-key and returns a new object which supports SpDataKey + /// for the specified sub-key. + /// + [PreserveSig] + public int OpenKey([MarshalAs(UnmanagedType.LPWStr)] string subKeyName, out ISpDataKey ppSubKey) + { + return _sapiRegKey.OpenKey(subKeyName, out ppSubKey); + } + + /// + /// Creates a sub-key and returns a new object which supports SpDataKey + /// for the specified sub-key. + /// + [PreserveSig] + public int CreateKey([MarshalAs(UnmanagedType.LPWStr)] string subKeyName, out ISpDataKey ppSubKey) + { + return _sapiRegKey.CreateKey(subKeyName, out ppSubKey); + } + + /// + /// Deletes the specified key. + /// + [PreserveSig] + public int DeleteKey([MarshalAs(UnmanagedType.LPWStr)] string subKeyName) + { + return _sapiRegKey.DeleteKey(subKeyName); + } + + /// + /// Deletes the specified value from the key. + /// + [PreserveSig] + public int DeleteValue([MarshalAs(UnmanagedType.LPWStr)] string valueName) + { + return _sapiRegKey.DeleteValue(valueName); + } + + /// + /// Retrieve a key name by index + /// + [PreserveSig] + public int EnumKeys(uint index, [MarshalAs(UnmanagedType.LPWStr)] out string ppszSubKeyName) + { + return _sapiRegKey.EnumKeys(index, out ppszSubKeyName); + } + + /// + /// Retrieves a key value by index + /// + [PreserveSig] + public int EnumValues(uint index, [MarshalAs(UnmanagedType.LPWStr)] out string valueName) + { + return _sapiRegKey.EnumValues(index, out valueName); + } + + #endregion + + /// + /// Full path and name for the key + /// + internal string Id + { + get + { + return _sKeyId; + } + } + + /// + /// Key Name (no path) + /// + internal string Name + { + get + { + int iPosSlash = _sKeyId.LastIndexOf('\\'); + return _sKeyId.Substring(iPosSlash + 1); + } + } + + // Disable parameter validation check + + /// + /// Reads the specified string value to the registry. If valueName is + /// NULL then the default value of the registry key is read. + /// + internal bool TryGetString(string valueName, out string value) + { + if (null == valueName) + { + valueName = string.Empty; + } + + return 0 == GetStringValue(valueName, out value); + } + + /// + /// Opens a sub-key and returns a new object which supports SpDataKey + /// for the specified sub-key. + /// + internal bool HasValue(string valueName) + { + string unusedString; + uint unusedUint = 0; + byte[] unusedBytes = Array.Empty(); + + return ( + 0 == _sapiRegKey.GetStringValue(valueName, out unusedString) || + 0 == _sapiRegKey.GetDWORD(valueName, ref unusedUint) || + 0 == _sapiRegKey.GetData(valueName, ref unusedUint, unusedBytes)); + } + + /// + /// Reads the specified DWORD from the registry. + /// + internal bool TryGetDWORD(string valueName, ref uint value) + { + if (string.IsNullOrEmpty(valueName)) + { + return false; + } + + return 0 == _sapiRegKey.GetDWORD(valueName, ref value); + } + + /// + /// Opens a sub-key and returns a new object which supports SpDataKey + /// for the specified sub-key. + /// + internal RegistryDataKey OpenKey(string keyName) + { + Helpers.ThrowIfEmptyOrNull(keyName, nameof(keyName)); + + ISpDataKey sapiSubKey; + if (0 != _sapiRegKey.OpenKey(keyName, out sapiSubKey)) + { + return null; + } + else + { + return new RegistryDataKey(_sKeyId + @"\" + keyName, sapiSubKey, true); + } + } + + /// + /// Creates a sub-key and returns a new object which supports SpDataKey + /// for the specified sub-key. + /// + internal RegistryDataKey CreateKey(string keyName) + { + Helpers.ThrowIfEmptyOrNull(keyName, nameof(keyName)); + + ISpDataKey sapiSubKey; + + if (0 != _sapiRegKey.CreateKey(keyName, out sapiSubKey)) + { + return null; + } + else + { + return new RegistryDataKey(_sKeyId + @"\" + keyName, sapiSubKey, true); + } + } + + /// + /// returns the name for all the values in this registry entry + /// + internal string[] GetValueNames() + { + List valueNames = new(); + + string valueName; + + for (uint i = 0; 0 == _sapiRegKey.EnumValues(i, out valueName); i++) + { + valueNames.Add(valueName); + } + + return valueNames.ToArray(); + } + + #region IEnumerable implementation + + IEnumerator IEnumerable.GetEnumerator() + { + string childKeyName = string.Empty; + + for (uint i = 0; 0 == _sapiRegKey.EnumKeys(i, out childKeyName); i++) + { + yield return this.CreateKey(childKeyName); + } + } + + IEnumerator IEnumerable.GetEnumerator() + { + return ((IEnumerable)this).GetEnumerator(); + } + + #endregion + + #endregion + + #region Protected Methods + protected virtual void Dispose(bool disposing) + { + if (disposing && _sapiRegKey != null && _disposeSapiKey) + { + Marshal.ReleaseComObject(_sapiRegKey); + _sapiRegKey = null; + } + } + + #endregion + + #region Internal Fields + + internal string _sKeyId; + + internal ISpDataKey _sapiRegKey; + + internal bool _disposeSapiKey; + + #endregion + + #region Private Methods + + private static SafeRegistryHandle RootHKEYFromRegPath(string rootPath) + { + RegistryKey rootKey = RegKeyFromRootPath(rootPath); + + if (null == rootKey) + { + return null; + } + + return rootKey.Handle; + } + + private static string GetFirstKeyAndParseRemainder(ref string registryPath) + { + int index = registryPath.IndexOf('\\'); + + string firstKey; + + if (index >= 0) + { + firstKey = registryPath.Substring(0, index); + registryPath = registryPath.Substring(index + 1, registryPath.Length - index - 1); + } + else + { + firstKey = registryPath; + registryPath = string.Empty; + } + + return firstKey; + } + + private static RegistryKey RegKeyFromRootPath(string rootPath) + { + RegistryKey[] roots = new RegistryKey[] { + Registry.ClassesRoot, + Registry.LocalMachine, + Registry.CurrentUser, + Registry.CurrentConfig + }; + + foreach (RegistryKey key in roots) + { + if (key.Name.Equals(rootPath, StringComparison.OrdinalIgnoreCase)) + { + return key; + } + } + + return null; + } + + #endregion + + #region private Types + + internal enum SAPIErrorCodes + { + STG_E_FILENOTFOUND = -2147287038, // 0x80030002 + SPERR_ALREADY_INITIALIZED = -2147201022, // 0x80045002 + SPERR_UNSUPPORTED_FORMAT = -2147201021, // 0x80045003 + SPERR_DEVICE_BUSY = -2147201018, // 0x80045006 + SPERR_DEVICE_NOT_SUPPORTED = -2147201017, // 0x80045007 + SPERR_DEVICE_NOT_ENABLED = -2147201016, // 0x80045008 + SPERR_NO_DRIVER = -2147201015, // 0x80045009 + SPERR_TOO_MANY_GRAMMARS = -2147200990, // 0x80045022 + SPERR_INVALID_IMPORT = -2147200988, // 0x80045024 + SPERR_AUDIO_BUFFER_OVERFLOW = -2147200977, // 0x8004502F + SPERR_NO_AUDIO_DATA = -2147200976, // 0x80045030 + SPERR_NO_MORE_ITEMS = -2147200967, // 0x80045039 + SPERR_NOT_FOUND = -2147200966, // 0x8004503A + SPERR_GENERIC_MMSYS_ERROR = -2147200964, // 0x8004503C + SPERR_NOT_TOPLEVEL_RULE = -2147200940, // 0x80045054 + SPERR_NOT_ACTIVE_SESSION = -2147200925, // 0x80045063 + SPERR_SML_GENERATION_FAIL = -2147200921, // 0x80045067 + SPERR_SHARED_ENGINE_DISABLED = -2147200906, // 0x80045076 + SPERR_RECOGNIZER_NOT_FOUND = -2147200905, // 0x80045077 + SPERR_AUDIO_NOT_FOUND = -2147200904, // 0x80045078 + S_OK = 0, // 0x00000000 + S_FALSE = 1, // 0x00000001 + E_INVALIDARG = -2147024809, // 0x80070057 + SP_NO_RULES_TO_ACTIVATE = 282747, // 0x0004507B + ERROR_MORE_DATA = 0x50EA, + } + + #endregion + } +} diff --git a/src/libraries/System.Speech/src/Internal/ObjectToken/SAPICategories.cs b/src/libraries/System.Speech/src/Internal/ObjectToken/SAPICategories.cs new file mode 100644 index 00000000000000..057ded55c2ff1d --- /dev/null +++ b/src/libraries/System.Speech/src/Internal/ObjectToken/SAPICategories.cs @@ -0,0 +1,329 @@ +// 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.Globalization; +using System.Speech.Internal.Synthesis; +using Microsoft.Win32; + +namespace System.Speech.Internal.ObjectTokens +{ + internal static class SAPICategories + { + #region internal Methods + + internal static ObjectToken DefaultToken(string category) + { + Helpers.ThrowIfEmptyOrNull(category, nameof(category)); + + ObjectToken token = null; + // Try first to get the preferred token for the current user + token = DefaultToken(@"HKEY_CURRENT_USER\SOFTWARE\Microsoft\Speech\" + category, _defaultTokenIdValueName); + + // IF failed try to get it for the local machine + if (token == null) + { + token = DefaultToken(SpeechRegistryKey + category, _defaultTokenIdValueName); + } + + return token; + } + + /// + /// Retrieve the Multimedia device ID. If the entry 'DefaultTokenId' is defined in the registry + /// under 'HKEY_CURRENT_USER\SOFTWARE\Microsoft\Speech\AudioOutput' then a multimedia device is looked + /// for with this token. Otherwise, picks the default WAVE_MAPPER is returned. + /// + internal static int DefaultDeviceOut() + { + int device = -1; + using (ObjectTokenCategory tokenCategory = ObjectTokenCategory.Create(@"HKEY_CURRENT_USER\SOFTWARE\Microsoft\Speech\AudioOutput")) + { + if (tokenCategory != null) + { + string deviceName; + if (tokenCategory.TryGetString(_defaultTokenIdValueName, out deviceName)) + { + int pos = deviceName.IndexOf('\\'); + if (pos > 0 && pos < deviceName.Length) + { + using (RegistryDataKey deviceKey = RegistryDataKey.Create(deviceName.Substring(pos + 1), Registry.LocalMachine)) + { + if (deviceKey != null) + { + device = AudioDeviceOut.GetDevicedId(deviceKey.Name); + } + } + } + } + } + } + + return device; + } + + #endregion + + private const string SpeechRegistryKey = @"HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Speech\"; + + internal const string CurrentUserVoices = @"HKEY_CURRENT_USER\SOFTWARE\Microsoft\Speech\Voices"; + + #region internal Fields + + internal const string Recognizers = SpeechRegistryKey + "Recognizers"; + internal const string Voices = SpeechRegistryKey + "Voices"; + + internal const string AudioIn = SpeechRegistryKey + "AudioInput"; + + #endregion + + #region Private Methods + + private static ObjectToken DefaultToken(string category, string defaultTokenIdValueName) + { + ObjectToken token = GetPreference(category, defaultTokenIdValueName); + + if (token != null) + { + // Now do special check to see if we have another token from the same vendor with a + // more recent version - if so use that. + + // First lets change the category to LOCAL_MACHINE + using (ObjectTokenCategory tokenCategory = ObjectTokenCategory.Create(category)) + { + if (tokenCategory != null) + { + if (token != null) + { + foreach (ObjectToken tokenSeed in (IEnumerable)tokenCategory) + { + token = GetHighestTokenVersion(token, tokenSeed, s_asVersionDefault); + } + } + else + { + // If there wasn't a default, just pick one with the proper culture + string[] sCultureId = new string[] { string.Format(CultureInfo.InvariantCulture, "{0:x}", CultureInfo.CurrentUICulture.LCID) }; + + foreach (ObjectToken tokenSeed in (IEnumerable)tokenCategory) + { + if (tokenSeed.MatchesAttributes(sCultureId)) + { + token = tokenSeed; + break; + } + } + + // Still nothing, picks the first one + if (token == null) + { + foreach (ObjectToken tokenSeed in (IEnumerable)tokenCategory) + { + token = tokenSeed; + break; + } + } + } + } + } + } + + return token; + } + + /// + /// Try to get the preferred token for a category + /// + private static ObjectToken GetPreference(string category, string defaultLocation) + { + ObjectToken token = null; + + using (ObjectTokenCategory tokenCategory = ObjectTokenCategory.Create(category)) + { + if (tokenCategory != null) + { + string sToken; + if (tokenCategory.TryGetString(defaultLocation, out sToken)) + { + token = tokenCategory.OpenToken(sToken); + } + } + } + return token; + } + + /// + /// Takes two tokens and compares them using version info. + /// Note only tokens that match on Vendor, ProductLine, Language get compared, the pfDidCompare flag indicates this + /// + private static int CompareTokenVersions(ObjectToken token1, ObjectToken token2, out bool pfDidCompare) + { + pfDidCompare = false; + + RegistryDataKey attributes1 = null; + RegistryDataKey attributes2 = null; + attributes1 = token1.Attributes; + attributes2 = token2.Attributes; + + // get vendor, version, language, product line for token 1 + if (attributes1 != null) + { + string vendor1; + string productLine1; + string version1; + string language1; + attributes1.TryGetString("Vendor", out vendor1); + attributes1.TryGetString("ProductLine", out productLine1); + attributes1.TryGetString("Version", out version1); + attributes1.TryGetString("Language", out language1); + + // get vendor, version, language, product line for token 2 + if (attributes2 != null) + { + string vendor2; + string productLine2; + string version2; + string language2; + attributes2.TryGetString("Vendor", out vendor2); + attributes2.TryGetString("ProductLine", out productLine2); + attributes2.TryGetString("Version", out version2); + attributes2.TryGetString("Language", out language2); + + if (((string.IsNullOrEmpty(vendor1) && string.IsNullOrEmpty(vendor2)) || (!string.IsNullOrEmpty(vendor1) && !string.IsNullOrEmpty(vendor2) && vendor1 == vendor2)) && + ((string.IsNullOrEmpty(productLine1) && string.IsNullOrEmpty(productLine2)) || (!string.IsNullOrEmpty(productLine1) && !string.IsNullOrEmpty(productLine2) && productLine1 == productLine2)) && + ((string.IsNullOrEmpty(language1) && string.IsNullOrEmpty(language2)) || (!string.IsNullOrEmpty(language1) && !string.IsNullOrEmpty(language2) && language1 == language2))) + { + pfDidCompare = true; + return CompareVersions(version1, version2); + } + else + { + return -1; + } + } + else + { + return 1; + } + } + else + { + return -1; + } + } + + /// + /// Takes two version number strings and compares them. + /// If V1 or V2 invalid format then the valid string is returned as being greater. + /// + private static int CompareVersions(string sV1, string sV2) + { + ushort[] v1 = new ushort[4]; + ushort[] v2 = new ushort[4]; + + bool fV1OK = ParseVersion(sV1, v1); + bool fV2OK = ParseVersion(sV2, v2); + + if (!fV1OK && !fV2OK) + { + return 0; + } + else if (fV1OK && !fV2OK) + { + return 1; + } + else if (!fV1OK && fV2OK) + { + return -1; + } + else + { + for (int ul = 0; ul < 4; ul++) + { + if (v1[ul] > v2[ul]) + { + return 1; + } + else if (v1[ul] < v2[ul]) + { + return -1; + } + } + } + + return 0; + } + + /// + /// Takes a version number string, checks it is valid, and fills the four + /// values in the Version array. Valid version stings are "a[.b[.c[.d]]]", + /// where a,b,c,d are +ve integers, 0 . 9999. If b,c,d are missing those + /// version values are set as zero. + /// + private static bool ParseVersion(string s, ushort[] Version) + { + bool fIsValid = true; + Version[0] = Version[1] = Version[2] = Version[3] = 0; + + if (string.IsNullOrEmpty(s)) + { + fIsValid = false; + } + else + { + int iPosPrev = 0; + for (int i = 0; i < 4 && iPosPrev < s.Length; i++) + { + int iPosDot = s.IndexOf('.', iPosPrev); + + // read +ve integer + string sInteger = s.Substring(iPosPrev, iPosDot); + ushort val; + + if (!ushort.TryParse(sInteger, out val) || val > 9999) + { + fIsValid = false; + break; + } + Version[i] = val; + + iPosPrev = iPosDot + 1; + } + + if (fIsValid && iPosPrev != s.Length) + { + fIsValid = false; + } + } + return fIsValid; + } + + private static ObjectToken GetHighestTokenVersion(ObjectToken token, ObjectToken tokenSeed, string[] criterias) + { + // if override and higher version - new preferred. + bool fOverride = tokenSeed.MatchesAttributes(criterias); + + if (fOverride) + { + bool fDidCompare; + int lRes = CompareTokenVersions(tokenSeed, token, out fDidCompare); + + if (fDidCompare && lRes > 0) + { + token = tokenSeed; + } + } + return token; + } + + #endregion + + #region private Fields + + private const string _defaultTokenIdValueName = "DefaultTokenId"; + + private static readonly string[] s_asVersionDefault = new string[] { "VersionDefault" }; + + #endregion + } +} diff --git a/src/libraries/System.Speech/src/Internal/PhonemeConverter.cs b/src/libraries/System.Speech/src/Internal/PhonemeConverter.cs new file mode 100644 index 00000000000000..8196685d9b78ad --- /dev/null +++ b/src/libraries/System.Speech/src/Internal/PhonemeConverter.cs @@ -0,0 +1,280 @@ +// 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.Text; + +namespace System.Speech.Internal +{ + internal sealed class PhonemeConverter + { + #region Constructors + + private PhonemeConverter(PhoneMap phoneMap) + { + _phoneMap = phoneMap; + } + + #endregion + + #region Internal methods + + /// + /// Returns the cached version of the universal phone converter. + /// + internal static PhonemeConverter UpsConverter + { + get + { + return s_upsConverter; + } + } + + /// + /// Convert a pronunciation string to code points + /// + internal static string ConvertPronToId(string pronunciation, int lcid) + { + PhonemeConverter phoneConv = UpsConverter; + foreach (PhoneMap phoneMap in s_phoneMaps) + { + if (phoneMap._lcid == lcid) + { + phoneConv = new PhonemeConverter(phoneMap); + } + } + + string phonemes = phoneConv.ConvertPronToId(pronunciation); + if (string.IsNullOrEmpty(phonemes)) + { + throw new FormatException(SR.Get(SRID.EmptyPronunciationString)); + } + return phonemes; + } + + /// + /// Convert an internal phone string to Id code string + /// The internal phones are space separated and may have a space + /// at the end. + /// + internal string ConvertPronToId(string sPhone) // Internal phone string + { + // remove the white spaces + sPhone = sPhone.Trim(Helpers._achTrimChars); + + // Empty Phoneme string + if (string.IsNullOrEmpty(sPhone)) + { + return string.Empty; + } + + int iPos = 0, iPosNext; + int cLen = sPhone.Length; + StringBuilder pidArray = new(cLen); + PhoneId phoneIdRef = new(); + + while (iPos < cLen) + { + iPosNext = sPhone.IndexOf(' ', iPos + 1); + if (iPosNext < 0) + { + iPosNext = cLen; + } + + string sCur = sPhone.Substring(iPos, iPosNext - iPos); + string sCurUpper = sCur.ToUpperInvariant(); + + // Search for this phone + phoneIdRef._phone = sCurUpper; + int index = Array.BinarySearch(_phoneMap._phoneIds, phoneIdRef, phoneIdRef); + if (index >= 0) + { + foreach (char ch in _phoneMap._phoneIds[index]._cp) + { + pidArray.Append(ch); + } + } + else + { + // phoneme not found error out + throw new FormatException(SR.Get(SRID.InvalidPhoneme, sCur)); + } + + iPos = iPosNext; + + // skip over the spaces + while (iPos < cLen && sPhone[iPos] == ' ') + { + iPos++; + } + } + + return pidArray.ToString(); + } /* CSpPhoneConverter::PhoneToId */ + + internal static void ValidateUpsIds(string ids) + { + ValidateUpsIds(ids.ToCharArray()); + } + + internal static void ValidateUpsIds(char[] ids) + { + foreach (char id in ids) + { + if (Array.BinarySearch(s_updIds, id) < 0) + { + throw new FormatException(SR.Get(SRID.InvalidPhoneme, id)); + } + } + } + + #endregion + + #region Private Methods + + /// + /// Builds the Phoneme maps from the compressed form. + /// + private static PhoneMap[] DecompressPhoneMaps(PhoneMapCompressed[] pmComps) + { + PhoneMap[] phoneMaps = new PhoneMap[pmComps.Length]; + + // Build the phoneme maps + for (int i = 0; i < pmComps.Length; i++) + { + PhoneMapCompressed pmCompressed = pmComps[i]; + PhoneMap pm = phoneMaps[i] = new PhoneMap(); + pm._lcid = pmCompressed._lcid; + pm._phoneIds = new PhoneId[pmCompressed._count]; + + int posPhone = 0; + int posCp = 0; + for (int j = 0; j < pm._phoneIds.Length; j++) + { + pm._phoneIds[j] = new PhoneId(); + // Count the number of chars in the phoneme string + int lastPhone; + int multi_phones = 0; + for (lastPhone = posPhone; pmCompressed._phones[lastPhone] != 0; lastPhone++) + { + // All phoneme code points are assumed to be of length == 1 + // if the length is greater, then a marker of -1 is set for each additional code points + if (pmCompressed._phones[lastPhone] == unchecked((byte)-1)) + { + multi_phones++; + } + } + + // Build the phoneme string + int strLen = lastPhone - posPhone - multi_phones; + char[] phone = new char[strLen]; + for (int l = 0; l < strLen; l++) + { + phone[l] = (char)pmCompressed._phones[posPhone++]; + } + + // Update the index for the next phoneme string + posPhone += multi_phones + 1; + + // Copy the code points for this phoneme + pm._phoneIds[j]._phone = new string(phone); + pm._phoneIds[j]._cp = new char[multi_phones + 1]; + for (int l = 0; l < pm._phoneIds[j]._cp.Length; l++) + { + pm._phoneIds[j]._cp[l] = pmCompressed._cps[posCp++]; + } + } + + // Ensure that the table is built properly + System.Diagnostics.Debug.Assert(posPhone == pmCompressed._phones.Length); + System.Diagnostics.Debug.Assert(posCp == pmCompressed._cps.Length); + } + return phoneMaps; + } + + // Do not delete generation of the phone conversion table from the registry entries + + #endregion + + #region Private Fields + + private PhoneMap _phoneMap; + + private static PhoneMapCompressed[] s_phoneMapsCompressed = new PhoneMapCompressed[] + { + new PhoneMapCompressed ( 0x0, 207, new byte [] {46, 0, 95, 33, 0, 95, 38, 0, 95, 44, 0, 95, 46, 0, 95, 63, 0, 95, 94, 0, 95, 124, 0, 95, 124, 124, 0, 95, 83, 0, 43, 0, 65, 0, 65, 65, 0, 65, 68, 86, 0, 65, 69, 0, 65, 69, 88, 0, 65, 72, 0, 65, 73, 255, 255, 0, 65, 78, 255, 0, 65, 79, 0, 65, 79, 69, 0, 65, 79, 88, 255, 255, 0, 65, 80, 73, 0, 65, 83, 80, 0, 65, 84, 82, 0, 65, 85, 255, 255, 0, 65, 88, 0, 65, 88, 82, 0, 66, 0, 66, 66, 0, 66, 72, 0, 66, 73, 77, 0, 66, 86, 65, 0, 66, 86, 68, 0, 67, 0, 67, 67, 255, 255, 0, 67, 67, 50, 0, 67, 67, 75, 0, 67, 69, 78, 0, 67, 72, 255, 255, 0, 67, 72, 50, 0, 67, 74, 0, 67, 84, 0, 67, 86, 68, 0, 68, 0, 68, 69, 78, 0, 68, 72, 0, 68, 73, 77, 0, 68, 82, 0, 68, 88, 0, 68, 88, 82, 0, 68, 90, 255, 255, 0, 68, 90, 50, 0, 69, 0, 69, 72, 0, 69, 72, 88, 255, 255, 0, 69, 73, 255, 255, 0, 69, 74, 67, 0, 69, 78, 255, 0, 69, 82, 0, 69, 82, 82, 0, 69, 83, 72, 0, 69, 84, 0, 69, 85, 0, 69, 88, 0, 69, 90, 72, 0, 70, 0, 71, 0, 71, 50, 0, 71, 65, 0, 71, 72, 0, 71, 73, 77, 0, 71, 76, 0, 71, 84, 0, 72, 0, 72, 71, 0, 72, 72, 0, 72, 76, 71, 0, 72, 90, 0, 73, 0, 73, 72, 0, 73, 88, 0, 73, 89, 88, 255, 255, 0, 74, 0, 74, 67, 255, 255, 0, 74, 67, 50, 0, 74, 68, 0, 74, 72, 255, 255, 0, 74, 72, 50, 0, 74, 73, 77, 0, 74, 74, 255, 255, 0, 75, 0, 76, 0, 76, 65, 66, 0, 76, 65, 77, 0, 76, 65, 82, 0, 76, 67, 75, 0, 76, 67, 75, 50, 0, 76, 71, 0, 76, 72, 0, 76, 74, 0, 76, 76, 65, 0, 76, 78, 71, 0, 76, 79, 87, 0, 76, 82, 0, 76, 82, 68, 0, 76, 83, 72, 0, 76, 84, 0, 77, 0, 77, 67, 78, 0, 77, 70, 0, 77, 82, 68, 0, 78, 0, 78, 65, 82, 0, 78, 65, 83, 0, 78, 67, 75, 0, 78, 67, 75, 50, 0, 78, 67, 75, 51, 0, 78, 71, 0, 78, 74, 0, 78, 82, 0, 78, 83, 82, 0, 78, 83, 89, 0, 79, 0, 79, 69, 0, 79, 69, 78, 255, 0, 79, 73, 255, 255, 0, 79, 78, 255, 0, 79, 85, 0, 79, 87, 88, 255, 255, 0, 79, 88, 0, 80, 0, 80, 65, 76, 0, 80, 67, 75, 0, 80, 70, 255, 255, 0, 80, 72, 0, 80, 72, 82, 0, 81, 0, 81, 68, 0, 81, 72, 0, 81, 73, 77, 0, 81, 78, 0, 81, 79, 77, 0, 81, 81, 0, 81, 84, 0, 82, 0, 82, 65, 0, 82, 65, 73, 0, 82, 69, 84, 0, 82, 72, 0, 82, 72, 79, 0, 82, 72, 90, 0, 82, 82, 0, 82, 84, 69, 0, 82, 84, 82, 0, 83, 0, 83, 49, 0, 83, 50, 0, 83, 67, 0, 83, 72, 0, 83, 72, 67, 0, 83, 72, 88, 0, 83, 82, 0, 83, 89, 76, 0, 84, 0, 84, 45, 0, 84, 43, 0, 84, 61, 0, 84, 49, 0, 84, 50, 0, 84, 51, 0, 84, 52, 0, 84, 53, 0, 84, 67, 75, 0, 84, 67, 75, 50, 0, 84, 72, 0, 84, 82, 0, 84, 83, 255, 255, 0, 84, 83, 50, 0, 84, 83, 82, 255, 255, 0, 85, 0, 85, 72, 0, 85, 82, 0, 85, 85, 0, 85, 87, 88, 255, 255, 0, 85, 89, 88, 255, 255, 0, 86, 0, 86, 65, 0, 86, 67, 68, 0, 86, 69, 76, 0, 86, 76, 83, 0, 86, 80, 72, 0, 86, 83, 76, 0, 87, 0, 87, 72, 0, 87, 74, 0, 88, 0, 88, 83, 72, 0, 88, 83, 84, 0, 89, 0, 89, 72, 0, 89, 88, 0, 90, 0, 90, 67, 0, 90, 72, 0, 90, 72, 74, 0, 90, 82, 0}, new char [] {(char) 46, (char) 1, (char) 2, (char) 3, (char) 8600, (char) 8599, (char) 8255, (char) 124, (char) 8214, (char) 4, (char) 865, (char) 97, (char) 593, (char) 799, (char) 230, (char) 592, (char) 652, (char) 97, (char) 865, (char) 105, (char) 97, (char) 771, (char) 596, (char) 630, (char) 596, (char) 865, (char) 601, (char) 826, (char) 688, (char) 792, (char) 97, (char) 865, (char) 650, (char) 601, (char) 602, (char) 98, (char) 665, (char) 946, (char) 595, (char) 689, (char) 804, (char) 231, (char) 1856, (char) 865, (char) 597, (char) 680, (char) 450, (char) 776, (char) 116, (char) 865, (char) 643, (char) 679, (char) 669, (char) 99, (char) 816, (char) 100, (char) 810, (char) 240, (char) 599, (char) 598, (char) 638, (char) 637, (char) 100, (char) 865, (char) 122, (char) 675, (char) 101, (char) 603, (char) 603, (char) 865, (char) 601, (char) 101, (char) 865, (char) 105, (char) 700, (char) 101, (char) 771, (char) 604, (char) 605, (char) 668, (char) 673, (char) 248, (char) 600, (char) 674, (char) 102, (char) 103, (char) 609, (char) 624, (char) 611, (char) 608, (char) 671, (char) 660, (char) 104, (char) 661, (char) 295, (char) 721, (char) 614, (char) 105, (char) 618, (char) 616, (char) 105, (char) 865, (char) 601, (char) 106, (char) 100, (char) 865, (char) 657, (char) 677, (char) 607, (char) 100, (char) 865, (char) 658, (char) 676, (char) 644, (char) 106, (char) 865, (char) 106, (char) 107, (char) 108, (char) 695, (char) 827, (char) 737, (char) 449, (char) 662, (char) 619, (char) 622, (char) 654, (char) 828, (char) 720, (char) 798, (char) 621, (char) 796, (char) 620, (char) 634, (char) 109, (char) 829, (char) 625, (char) 825, (char) 110, (char) 794, (char) 771, (char) 33, (char) 451, (char) 663, (char) 331, (char) 626, (char) 627, (char) 8319, (char) 815, (char) 111, (char) 339, (char) 339, (char) 771, (char) 596, (char) 865, (char) 105, (char) 111, (char) 771, (char) 612, (char) 111, (char) 865, (char) 601, (char) 629, (char) 112, (char) 690, (char) 664, (char) 112, (char) 865, (char) 102, (char) 632, (char) 740, (char) 594, (char) 610, (char) 967, (char) 667, (char) 628, (char) 672, (char) 640, (char) 113, (char) 635, (char) 633, (char) 797, (char) 817, (char) 641, (char) 734, (char) 692, (char) 114, (char) 800, (char) 793, (char) 115, (char) 712, (char) 716, (char) 597, (char) 643, (char) 646, (char) 615, (char) 642, (char) 809, (char) 116, (char) 8595, (char) 8593, (char) 8594, (char) 783, (char) 768, (char) 772, (char) 769, (char) 779, (char) 448, (char) 647, (char) 952, (char) 648, (char) 116, (char) 865, (char) 115, (char) 678, (char) 116, (char) 865, (char) 642, (char) 117, (char) 650, (char) 606, (char) 623, (char) 117, (char) 865, (char) 601, (char) 121, (char) 865, (char) 601, (char) 118, (char) 651, (char) 812, (char) 736, (char) 778, (char) 820, (char) 805, (char) 119, (char) 653, (char) 613, (char) 120, (char) 728, (char) 774, (char) 121, (char) 655, (char) 649, (char) 122, (char) 657, (char) 658, (char) 659, (char) 656}), + new PhoneMapCompressed ( 0x404, 52, new byte [] {48, 48, 50, 49, 0, 48, 48, 50, 54, 0, 48, 48, 50, 65, 0, 48, 48, 50, 66, 0, 48, 48, 50, 67, 0, 48, 48, 50, 68, 0, 48, 48, 50, 69, 0, 48, 48, 51, 70, 0, 48, 48, 53, 70, 0, 48, 50, 67, 55, 0, 48, 50, 67, 57, 0, 48, 50, 67, 65, 0, 48, 50, 67, 66, 0, 48, 50, 68, 57, 0, 51, 48, 48, 48, 0, 51, 49, 48, 53, 0, 51, 49, 48, 54, 0, 51, 49, 48, 55, 0, 51, 49, 48, 56, 0, 51, 49, 48, 57, 0, 51, 49, 48, 65, 0, 51, 49, 48, 66, 0, 51, 49, 48, 67, 0, 51, 49, 48, 68, 0, 51, 49, 48, 69, 0, 51, 49, 48, 70, 0, 51, 49, 49, 48, 0, 51, 49, 49, 49, 0, 51, 49, 49, 50, 0, 51, 49, 49, 51, 0, 51, 49, 49, 52, 0, 51, 49, 49, 53, 0, 51, 49, 49, 54, 0, 51, 49, 49, 55, 0, 51, 49, 49, 56, 0, 51, 49, 49, 57, 0, 51, 49, 49, 65, 0, 51, 49, 49, 66, 0, 51, 49, 49, 67, 0, 51, 49, 49, 68, 0, 51, 49, 49, 69, 0, 51, 49, 49, 70, 0, 51, 49, 50, 48, 0, 51, 49, 50, 49, 0, 51, 49, 50, 50, 0, 51, 49, 50, 51, 0, 51, 49, 50, 52, 0, 51, 49, 50, 53, 0, 51, 49, 50, 54, 0, 51, 49, 50, 55, 0, 51, 49, 50, 56, 0, 51, 49, 50, 57, 0}, new char [] {(char) 33, (char) 38, (char) 42, (char) 43, (char) 44, (char) 45, (char) 46, (char) 63, (char) 95, (char) 711, (char) 713, (char) 714, (char) 715, (char) 729, (char) 12288, (char) 12549, (char) 12550, (char) 12551, (char) 12552, (char) 12553, (char) 12554, (char) 12555, (char) 12556, (char) 12557, (char) 12558, (char) 12559, (char) 12560, (char) 12561, (char) 12562, (char) 12563, (char) 12564, (char) 12565, (char) 12566, (char) 12567, (char) 12568, (char) 12569, (char) 12570, (char) 12571, (char) 12572, (char) 12573, (char) 12574, (char) 12575, (char) 12576, (char) 12577, (char) 12578, (char) 12579, (char) 12580, (char) 12581, (char) 12582, (char) 12583, (char) 12584, (char) 12585}), + new PhoneMapCompressed ( 0x407, 53, new byte [] {45, 0, 33, 0, 38, 0, 44, 0, 46, 0, 58, 0, 63, 0, 94, 0, 95, 0, 126, 0, 49, 0, 50, 0, 65, 0, 65, 87, 0, 65, 88, 0, 65, 89, 0, 66, 0, 67, 72, 0, 68, 0, 69, 72, 0, 69, 85, 0, 69, 89, 0, 70, 0, 71, 0, 72, 0, 73, 72, 0, 73, 89, 0, 74, 72, 0, 75, 0, 76, 0, 77, 0, 78, 0, 78, 71, 0, 79, 69, 0, 79, 72, 0, 79, 87, 0, 79, 89, 0, 80, 0, 80, 70, 0, 82, 0, 83, 0, 83, 72, 0, 84, 0, 84, 83, 0, 85, 69, 0, 85, 72, 0, 85, 87, 0, 85, 89, 0, 86, 0, 88, 0, 89, 0, 90, 0, 90, 72, 0}, new char [] {(char) 1, (char) 2, (char) 3, (char) 4, (char) 5, (char) 12, (char) 6, (char) 8, (char) 7, (char) 11, (char) 9, (char) 10, (char) 13, (char) 14, (char) 15, (char) 16, (char) 17, (char) 19, (char) 18, (char) 20, (char) 21, (char) 22, (char) 23, (char) 24, (char) 25, (char) 26, (char) 27, (char) 28, (char) 29, (char) 30, (char) 31, (char) 32, (char) 33, (char) 34, (char) 35, (char) 36, (char) 37, (char) 38, (char) 39, (char) 40, (char) 41, (char) 42, (char) 43, (char) 44, (char) 45, (char) 46, (char) 47, (char) 48, (char) 49, (char) 50, (char) 51, (char) 52, (char) 53}), + new PhoneMapCompressed ( 0x409, 49, new byte [] {45, 0, 33, 0, 38, 0, 44, 0, 46, 0, 63, 0, 95, 0, 49, 0, 50, 0, 65, 65, 0, 65, 69, 0, 65, 72, 0, 65, 79, 0, 65, 87, 0, 65, 88, 0, 65, 89, 0, 66, 0, 67, 72, 0, 68, 0, 68, 72, 0, 69, 72, 0, 69, 82, 0, 69, 89, 0, 70, 0, 71, 0, 72, 0, 73, 72, 0, 73, 89, 0, 74, 72, 0, 75, 0, 76, 0, 77, 0, 78, 0, 78, 71, 0, 79, 87, 0, 79, 89, 0, 80, 0, 82, 0, 83, 0, 83, 72, 0, 84, 0, 84, 72, 0, 85, 72, 0, 85, 87, 0, 86, 0, 87, 0, 89, 0, 90, 0, 90, 72, 0}, new char [] {(char) 1, (char) 2, (char) 3, (char) 4, (char) 5, (char) 6, (char) 7, (char) 8, (char) 9, (char) 10, (char) 11, (char) 12, (char) 13, (char) 14, (char) 15, (char) 16, (char) 17, (char) 18, (char) 19, (char) 20, (char) 21, (char) 22, (char) 23, (char) 24, (char) 25, (char) 26, (char) 27, (char) 28, (char) 29, (char) 30, (char) 31, (char) 32, (char) 33, (char) 34, (char) 35, (char) 36, (char) 37, (char) 38, (char) 39, (char) 40, (char) 41, (char) 42, (char) 43, (char) 44, (char) 45, (char) 46, (char) 47, (char) 48, (char) 49}), + new PhoneMapCompressed ( 0x40A, 35, new byte [] {45, 0, 33, 0, 38, 0, 44, 0, 46, 0, 63, 0, 95, 0, 49, 0, 50, 0, 65, 0, 66, 0, 67, 72, 0, 68, 0, 69, 0, 70, 0, 71, 0, 73, 0, 74, 0, 74, 74, 0, 75, 0, 76, 0, 76, 76, 0, 77, 0, 78, 0, 78, 74, 0, 79, 0, 80, 0, 82, 0, 82, 82, 0, 83, 0, 84, 0, 84, 72, 0, 85, 0, 87, 0, 88, 0}, new char [] {(char) 1, (char) 2, (char) 3, (char) 4, (char) 5, (char) 6, (char) 7, (char) 8, (char) 9, (char) 10, (char) 18, (char) 21, (char) 16, (char) 11, (char) 23, (char) 20, (char) 12, (char) 33, (char) 22, (char) 19, (char) 29, (char) 30, (char) 26, (char) 27, (char) 28, (char) 13, (char) 17, (char) 31, (char) 32, (char) 24, (char) 15, (char) 35, (char) 14, (char) 34, (char) 25}), + new PhoneMapCompressed ( 0x40C, 42, new byte [] {45, 0, 33, 0, 38, 0, 44, 0, 46, 0, 63, 0, 95, 0, 126, 0, 49, 0, 65, 0, 65, 65, 0, 65, 88, 0, 66, 0, 68, 0, 69, 72, 0, 69, 85, 0, 69, 89, 0, 70, 0, 71, 0, 72, 89, 0, 73, 89, 0, 75, 0, 76, 0, 77, 0, 78, 0, 78, 71, 0, 78, 74, 0, 79, 69, 0, 79, 72, 0, 79, 87, 0, 80, 0, 82, 0, 83, 0, 83, 72, 0, 84, 0, 85, 87, 0, 85, 89, 0, 86, 0, 87, 0, 89, 0, 90, 0, 90, 72, 0}, new char [] {(char) 1, (char) 2, (char) 3, (char) 4, (char) 5, (char) 6, (char) 7, (char) 9, (char) 8, (char) 11, (char) 10, (char) 13, (char) 14, (char) 15, (char) 16, (char) 30, (char) 17, (char) 18, (char) 19, (char) 20, (char) 22, (char) 23, (char) 24, (char) 25, (char) 26, (char) 27, (char) 28, (char) 29, (char) 12, (char) 31, (char) 32, (char) 33, (char) 34, (char) 35, (char) 36, (char) 37, (char) 21, (char) 38, (char) 39, (char) 40, (char) 41, (char) 42}), + new PhoneMapCompressed ( 0x411, 102, new byte [] {48, 48, 50, 49, 0, 48, 48, 50, 55, 0, 48, 48, 50, 66, 0, 48, 48, 50, 69, 0, 48, 48, 51, 70, 0, 48, 48, 53, 70, 0, 48, 48, 55, 67, 0, 51, 48, 57, 67, 0, 51, 48, 65, 49, 0, 51, 48, 65, 50, 0, 51, 48, 65, 51, 0, 51, 48, 65, 52, 0, 51, 48, 65, 53, 0, 51, 48, 65, 54, 0, 51, 48, 65, 55, 0, 51, 48, 65, 56, 0, 51, 48, 65, 57, 0, 51, 48, 65, 65, 0, 51, 48, 65, 66, 0, 51, 48, 65, 67, 0, 51, 48, 65, 68, 0, 51, 48, 65, 69, 0, 51, 48, 65, 70, 0, 51, 48, 66, 48, 0, 51, 48, 66, 49, 0, 51, 48, 66, 50, 0, 51, 48, 66, 51, 0, 51, 48, 66, 52, 0, 51, 48, 66, 53, 0, 51, 48, 66, 54, 0, 51, 48, 66, 55, 0, 51, 48, 66, 56, 0, 51, 48, 66, 57, 0, 51, 48, 66, 65, 0, 51, 48, 66, 66, 0, 51, 48, 66, 67, 0, 51, 48, 66, 68, 0, 51, 48, 66, 69, 0, 51, 48, 66, 70, 0, 51, 48, 67, 48, 0, 51, 48, 67, 49, 0, 51, 48, 67, 50, 0, 51, 48, 67, 51, 0, 51, 48, 67, 52, 0, 51, 48, 67, 53, 0, 51, 48, 67, 54, 0, 51, 48, 67, 55, 0, 51, 48, 67, 56, 0, 51, 48, 67, 57, 0, 51, 48, 67, 65, 0, 51, 48, 67, 66, 0, 51, 48, 67, 67, 0, 51, 48, 67, 68, 0, 51, 48, 67, 69, 0, 51, 48, 67, 70, 0, 51, 48, 68, 48, 0, 51, 48, 68, 49, 0, 51, 48, 68, 50, 0, 51, 48, 68, 51, 0, 51, 48, 68, 52, 0, 51, 48, 68, 53, 0, 51, 48, 68, 54, 0, 51, 48, 68, 55, 0, 51, 48, 68, 56, 0, 51, 48, 68, 57, 0, 51, 48, 68, 65, 0, 51, 48, 68, 66, 0, 51, 48, 68, 67, 0, 51, 48, 68, 68, 0, 51, 48, 68, 69, 0, 51, 48, 68, 70, 0, 51, 48, 69, 48, 0, 51, 48, 69, 49, 0, 51, 48, 69, 50, 0, 51, 48, 69, 51, 0, 51, 48, 69, 52, 0, 51, 48, 69, 53, 0, 51, 48, 69, 54, 0, 51, 48, 69, 55, 0, 51, 48, 69, 56, 0, 51, 48, 69, 57, 0, 51, 48, 69, 65, 0, 51, 48, 69, 66, 0, 51, 48, 69, 67, 0, 51, 48, 69, 68, 0, 51, 48, 69, 69, 0, 51, 48, 69, 70, 0, 51, 48, 70, 48, 0, 51, 48, 70, 49, 0, 51, 48, 70, 50, 0, 51, 48, 70, 51, 0, 51, 48, 70, 52, 0, 51, 48, 70, 53, 0, 51, 48, 70, 54, 0, 51, 48, 70, 55, 0, 51, 48, 70, 56, 0, 51, 48, 70, 57, 0, 51, 48, 70, 65, 0, 51, 48, 70, 66, 0, 51, 48, 70, 67, 0, 51, 48, 70, 68, 0, 51, 48, 70, 69, 0}, new char [] {(char) 33, (char) 39, (char) 43, (char) 46, (char) 63, (char) 95, (char) 124, (char) 12444, (char) 12449, (char) 12450, (char) 12451, (char) 12452, (char) 12453, (char) 12454, (char) 12455, (char) 12456, (char) 12457, (char) 12458, (char) 12459, (char) 12460, (char) 12461, (char) 12462, (char) 12463, (char) 12464, (char) 12465, (char) 12466, (char) 12467, (char) 12468, (char) 12469, (char) 12470, (char) 12471, (char) 12472, (char) 12473, (char) 12474, (char) 12475, (char) 12476, (char) 12477, (char) 12478, (char) 12479, (char) 12480, (char) 12481, (char) 12482, (char) 12483, (char) 12484, (char) 12485, (char) 12486, (char) 12487, (char) 12488, (char) 12489, (char) 12490, (char) 12491, (char) 12492, (char) 12493, (char) 12494, (char) 12495, (char) 12496, (char) 12497, (char) 12498, (char) 12499, (char) 12500, (char) 12501, (char) 12502, (char) 12503, (char) 12504, (char) 12505, (char) 12506, (char) 12507, (char) 12508, (char) 12509, (char) 12510, (char) 12511, (char) 12512, (char) 12513, (char) 12514, (char) 12515, (char) 12516, (char) 12517, (char) 12518, (char) 12519, (char) 12520, (char) 12521, (char) 12522, (char) 12523, (char) 12524, (char) 12525, (char) 12526, (char) 12527, (char) 12528, (char) 12529, (char) 12530, (char) 12531, (char) 12532, (char) 12533, (char) 12534, (char) 12535, (char) 12536, (char) 12537, (char) 12538, (char) 12539, (char) 12540, (char) 12541, (char) 12542}), + new PhoneMapCompressed ( 0x804, 422, new byte [] {45, 0, 33, 0, 38, 0, 42, 0, 44, 0, 46, 0, 63, 0, 95, 0, 43, 0, 49, 0, 50, 0, 51, 0, 52, 0, 53, 0, 65, 0, 65, 73, 0, 65, 78, 0, 65, 78, 71, 0, 65, 79, 0, 66, 65, 0, 66, 65, 73, 0, 66, 65, 78, 0, 66, 65, 78, 71, 0, 66, 65, 79, 0, 66, 69, 73, 0, 66, 69, 78, 0, 66, 69, 78, 71, 0, 66, 73, 0, 66, 73, 65, 78, 0, 66, 73, 65, 79, 0, 66, 73, 69, 0, 66, 73, 78, 0, 66, 73, 78, 71, 0, 66, 79, 0, 66, 85, 0, 67, 65, 0, 67, 65, 73, 0, 67, 65, 78, 0, 67, 65, 78, 71, 0, 67, 65, 79, 0, 67, 69, 0, 67, 69, 78, 0, 67, 69, 78, 71, 0, 67, 72, 65, 0, 67, 72, 65, 73, 0, 67, 72, 65, 78, 0, 67, 72, 65, 78, 71, 0, 67, 72, 65, 79, 0, 67, 72, 69, 0, 67, 72, 69, 78, 0, 67, 72, 69, 78, 71, 0, 67, 72, 73, 0, 67, 72, 79, 78, 71, 0, 67, 72, 79, 85, 0, 67, 72, 85, 0, 67, 72, 85, 65, 73, 0, 67, 72, 85, 65, 78, 0, 67, 72, 85, 65, 78, 71, 0, 67, 72, 85, 73, 0, 67, 72, 85, 78, 0, 67, 72, 85, 79, 0, 67, 73, 0, 67, 79, 78, 71, 0, 67, 79, 85, 0, 67, 85, 0, 67, 85, 65, 78, 0, 67, 85, 73, 0, 67, 85, 78, 0, 67, 85, 79, 0, 68, 65, 0, 68, 65, 73, 0, 68, 65, 78, 0, 68, 65, 78, 71, 0, 68, 65, 79, 0, 68, 69, 0, 68, 69, 73, 0, 68, 69, 78, 0, 68, 69, 78, 71, 0, 68, 73, 0, 68, 73, 65, 0, 68, 73, 65, 78, 0, 68, 73, 65, 79, 0, 68, 73, 69, 0, 68, 73, 78, 71, 0, 68, 73, 85, 0, 68, 79, 78, 71, 0, 68, 79, 85, 0, 68, 85, 0, 68, 85, 65, 78, 0, 68, 85, 73, 0, 68, 85, 78, 0, 68, 85, 79, 0, 69, 0, 69, 73, 0, 69, 78, 0, 69, 82, 0, 70, 65, 0, 70, 65, 78, 0, 70, 65, 78, 71, 0, 70, 69, 73, 0, 70, 69, 78, 0, 70, 69, 78, 71, 0, 70, 79, 0, 70, 79, 85, 0, 70, 85, 0, 71, 65, 0, 71, 65, 73, 0, 71, 65, 78, 0, 71, 65, 78, 71, 0, 71, 65, 79, 0, 71, 69, 0, 71, 69, 73, 0, 71, 69, 78, 0, 71, 69, 78, 71, 0, 71, 79, 78, 71, 0, 71, 79, 85, 0, 71, 85, 0, 71, 85, 65, 0, 71, 85, 65, 73, 0, 71, 85, 65, 78, 0, 71, 85, 65, 78, 71, 0, 71, 85, 73, 0, 71, 85, 78, 0, 71, 85, 79, 0, 72, 65, 0, 72, 65, 73, 0, 72, 65, 78, 0, 72, 65, 78, 71, 0, 72, 65, 79, 0, 72, 69, 0, 72, 69, 73, 0, 72, 69, 78, 0, 72, 69, 78, 71, 0, 72, 79, 78, 71, 0, 72, 79, 85, 0, 72, 85, 0, 72, 85, 65, 0, 72, 85, 65, 73, 0, 72, 85, 65, 78, 0, 72, 85, 65, 78, 71, 0, 72, 85, 73, 0, 72, 85, 78, 0, 72, 85, 79, 0, 74, 73, 0, 74, 73, 65, 0, 74, 73, 65, 78, 0, 74, 73, 65, 78, 71, 0, 74, 73, 65, 79, 0, 74, 73, 69, 0, 74, 73, 78, 0, 74, 73, 78, 71, 0, 74, 73, 79, 78, 71, 0, 74, 73, 85, 0, 74, 85, 0, 74, 85, 65, 78, 0, 74, 85, 69, 0, 74, 85, 78, 0, 75, 65, 0, 75, 65, 73, 0, 75, 65, 78, 0, 75, 65, 78, 71, 0, 75, 65, 79, 0, 75, 69, 0, 75, 69, 73, 0, 75, 69, 78, 0, 75, 69, 78, 71, 0, 75, 79, 78, 71, 0, 75, 79, 85, 0, 75, 85, 0, 75, 85, 65, 0, 75, 85, 65, 73, 0, 75, 85, 65, 78, 0, 75, 85, 65, 78, 71, 0, 75, 85, 73, 0, 75, 85, 78, 0, 75, 85, 79, 0, 76, 65, 0, 76, 65, 73, 0, 76, 65, 78, 0, 76, 65, 78, 71, 0, 76, 65, 79, 0, 76, 69, 0, 76, 69, 73, 0, 76, 69, 78, 71, 0, 76, 73, 0, 76, 73, 65, 0, 76, 73, 65, 78, 0, 76, 73, 65, 78, 71, 0, 76, 73, 65, 79, 0, 76, 73, 69, 0, 76, 73, 78, 0, 76, 73, 78, 71, 0, 76, 73, 85, 0, 76, 79, 0, 76, 79, 78, 71, 0, 76, 79, 85, 0, 76, 85, 0, 76, 85, 65, 78, 0, 76, 85, 69, 0, 76, 85, 78, 0, 76, 85, 79, 0, 76, 86, 0, 77, 65, 0, 77, 65, 73, 0, 77, 65, 78, 0, 77, 65, 78, 71, 0, 77, 65, 79, 0, 77, 69, 0, 77, 69, 73, 0, 77, 69, 78, 0, 77, 69, 78, 71, 0, 77, 73, 0, 77, 73, 65, 78, 0, 77, 73, 65, 79, 0, 77, 73, 69, 0, 77, 73, 78, 0, 77, 73, 78, 71, 0, 77, 73, 85, 0, 77, 79, 0, 77, 79, 85, 0, 77, 85, 0, 78, 65, 0, 78, 65, 73, 0, 78, 65, 78, 0, 78, 65, 78, 71, 0, 78, 65, 79, 0, 78, 69, 0, 78, 69, 73, 0, 78, 69, 78, 0, 78, 69, 78, 71, 0, 78, 73, 0, 78, 73, 65, 78, 0, 78, 73, 65, 78, 71, 0, 78, 73, 65, 79, 0, 78, 73, 69, 0, 78, 73, 78, 0, 78, 73, 78, 71, 0, 78, 73, 85, 0, 78, 79, 78, 71, 0, 78, 79, 85, 0, 78, 85, 0, 78, 85, 65, 78, 0, 78, 85, 69, 0, 78, 85, 79, 0, 78, 86, 0, 79, 0, 79, 85, 0, 80, 65, 0, 80, 65, 73, 0, 80, 65, 78, 0, 80, 65, 78, 71, 0, 80, 65, 79, 0, 80, 69, 73, 0, 80, 69, 78, 0, 80, 69, 78, 71, 0, 80, 73, 0, 80, 73, 65, 78, 0, 80, 73, 65, 79, 0, 80, 73, 69, 0, 80, 73, 78, 0, 80, 73, 78, 71, 0, 80, 79, 0, 80, 79, 85, 0, 80, 85, 0, 81, 73, 0, 81, 73, 65, 0, 81, 73, 65, 78, 0, 81, 73, 65, 78, 71, 0, 81, 73, 65, 79, 0, 81, 73, 69, 0, 81, 73, 78, 0, 81, 73, 78, 71, 0, 81, 73, 79, 78, 71, 0, 81, 73, 85, 0, 81, 85, 0, 81, 85, 65, 78, 0, 81, 85, 69, 0, 81, 85, 78, 0, 82, 65, 78, 0, 82, 65, 78, 71, 0, 82, 65, 79, 0, 82, 69, 0, 82, 69, 78, 0, 82, 69, 78, 71, 0, 82, 73, 0, 82, 79, 78, 71, 0, 82, 79, 85, 0, 82, 85, 0, 82, 85, 65, 78, 0, 82, 85, 73, 0, 82, 85, 78, 0, 82, 85, 79, 0, 83, 65, 0, 83, 65, 73, 0, 83, 65, 78, 0, 83, 65, 78, 71, 0, 83, 65, 79, 0, 83, 69, 0, 83, 69, 78, 0, 83, 69, 78, 71, 0, 83, 72, 65, 0, 83, 72, 65, 73, 0, 83, 72, 65, 78, 0, 83, 72, 65, 78, 71, 0, 83, 72, 65, 79, 0, 83, 72, 69, 0, 83, 72, 69, 73, 0, 83, 72, 69, 78, 0, 83, 72, 69, 78, 71, 0, 83, 72, 73, 0, 83, 72, 79, 85, 0, 83, 72, 85, 0, 83, 72, 85, 65, 0, 83, 72, 85, 65, 73, 0, 83, 72, 85, 65, 78, 0, 83, 72, 85, 65, 78, 71, 0, 83, 72, 85, 73, 0, 83, 72, 85, 78, 0, 83, 72, 85, 79, 0, 83, 73, 0, 83, 79, 78, 71, 0, 83, 79, 85, 0, 83, 85, 0, 83, 85, 65, 78, 0, 83, 85, 73, 0, 83, 85, 78, 0, 83, 85, 79, 0, 84, 65, 0, 84, 65, 73, 0, 84, 65, 78, 0, 84, 65, 78, 71, 0, 84, 65, 79, 0, 84, 69, 0, 84, 69, 73, 0, 84, 69, 78, 71, 0, 84, 73, 0, 84, 73, 65, 78, 0, 84, 73, 65, 79, 0, 84, 73, 69, 0, 84, 73, 78, 71, 0, 84, 79, 78, 71, 0, 84, 79, 85, 0, 84, 85, 0, 84, 85, 65, 78, 0, 84, 85, 73, 0, 84, 85, 78, 0, 84, 85, 79, 0, 87, 65, 0, 87, 65, 73, 0, 87, 65, 78, 0, 87, 65, 78, 71, 0, 87, 69, 73, 0, 87, 69, 78, 0, 87, 69, 78, 71, 0, 87, 79, 0, 87, 85, 0, 88, 73, 0, 88, 73, 65, 0, 88, 73, 65, 78, 0, 88, 73, 65, 78, 71, 0, 88, 73, 65, 79, 0, 88, 73, 69, 0, 88, 73, 78, 0, 88, 73, 78, 71, 0, 88, 73, 79, 78, 71, 0, 88, 73, 85, 0, 88, 85, 0, 88, 85, 65, 78, 0, 88, 85, 69, 0, 88, 85, 78, 0, 89, 65, 0, 89, 65, 78, 0, 89, 65, 78, 71, 0, 89, 65, 79, 0, 89, 69, 0, 89, 73, 0, 89, 73, 78, 0, 89, 73, 78, 71, 0, 89, 79, 0, 89, 79, 78, 71, 0, 89, 79, 85, 0, 89, 85, 0, 89, 85, 65, 78, 0, 89, 85, 69, 0, 89, 85, 78, 0, 90, 65, 0, 90, 65, 73, 0, 90, 65, 78, 0, 90, 65, 78, 71, 0, 90, 65, 79, 0, 90, 69, 0, 90, 69, 73, 0, 90, 69, 78, 0, 90, 69, 78, 71, 0, 90, 72, 65, 0, 90, 72, 65, 73, 0, 90, 72, 65, 78, 0, 90, 72, 65, 78, 71, 0, 90, 72, 65, 79, 0, 90, 72, 69, 0, 90, 72, 69, 73, 0, 90, 72, 69, 78, 0, 90, 72, 69, 78, 71, 0, 90, 72, 73, 0, 90, 72, 79, 78, 71, 0, 90, 72, 79, 85, 0, 90, 72, 85, 0, 90, 72, 85, 65, 0, 90, 72, 85, 65, 73, 0, 90, 72, 85, 65, 78, 0, 90, 72, 85, 65, 78, 71, 0, 90, 72, 85, 73, 0, 90, 72, 85, 78, 0, 90, 72, 85, 79, 0, 90, 73, 0, 90, 79, 78, 71, 0, 90, 79, 85, 0, 90, 85, 0, 90, 85, 65, 78, 0, 90, 85, 73, 0, 90, 85, 78, 0, 90, 85, 79, 0}, new char [] {(char) 1, (char) 2, (char) 3, (char) 9, (char) 4, (char) 5, (char) 6, (char) 7, (char) 8, (char) 10, (char) 11, (char) 12, (char) 13, (char) 14, (char) 15, (char) 16, (char) 17, (char) 18, (char) 19, (char) 20, (char) 21, (char) 22, (char) 23, (char) 24, (char) 25, (char) 26, (char) 27, (char) 28, (char) 29, (char) 30, (char) 31, (char) 32, (char) 33, (char) 34, (char) 35, (char) 36, (char) 37, (char) 38, (char) 39, (char) 40, (char) 41, (char) 42, (char) 43, (char) 44, (char) 45, (char) 46, (char) 47, (char) 48, (char) 49, (char) 50, (char) 51, (char) 52, (char) 53, (char) 54, (char) 55, (char) 56, (char) 57, (char) 58, (char) 59, (char) 60, (char) 61, (char) 62, (char) 63, (char) 64, (char) 65, (char) 66, (char) 67, (char) 68, (char) 69, (char) 70, (char) 71, (char) 72, (char) 73, (char) 74, (char) 75, (char) 76, (char) 77, (char) 78, (char) 79, (char) 80, (char) 81, (char) 82, (char) 83, (char) 84, (char) 85, (char) 86, (char) 87, (char) 88, (char) 89, (char) 90, (char) 91, (char) 92, (char) 93, (char) 94, (char) 95, (char) 96, (char) 97, (char) 98, (char) 99, (char) 100, (char) 101, (char) 102, (char) 103, (char) 104, (char) 105, (char) 106, (char) 107, (char) 108, (char) 109, (char) 110, (char) 111, (char) 112, (char) 113, (char) 114, (char) 115, (char) 116, (char) 117, (char) 118, (char) 119, (char) 120, (char) 121, (char) 122, (char) 123, (char) 124, (char) 125, (char) 126, (char) 127, (char) 128, (char) 129, (char) 130, (char) 131, (char) 132, (char) 133, (char) 134, (char) 135, (char) 136, (char) 137, (char) 138, (char) 139, (char) 140, (char) 141, (char) 142, (char) 143, (char) 144, (char) 145, (char) 146, (char) 147, (char) 148, (char) 149, (char) 150, (char) 151, (char) 152, (char) 153, (char) 154, (char) 155, (char) 156, (char) 157, (char) 158, (char) 159, (char) 160, (char) 161, (char) 162, (char) 163, (char) 164, (char) 165, (char) 166, (char) 167, (char) 168, (char) 169, (char) 170, (char) 171, (char) 172, (char) 173, (char) 174, (char) 175, (char) 176, (char) 177, (char) 178, (char) 179, (char) 180, (char) 181, (char) 182, (char) 183, (char) 184, (char) 185, (char) 186, (char) 187, (char) 188, (char) 189, (char) 190, (char) 191, (char) 192, (char) 193, (char) 194, (char) 195, (char) 196, (char) 197, (char) 198, (char) 199, (char) 200, (char) 201, (char) 202, (char) 203, (char) 204, (char) 205, (char) 206, (char) 207, (char) 208, (char) 209, (char) 210, (char) 211, (char) 212, (char) 213, (char) 214, (char) 215, (char) 216, (char) 217, (char) 218, (char) 219, (char) 220, (char) 221, (char) 222, (char) 223, (char) 224, (char) 225, (char) 226, (char) 227, (char) 228, (char) 229, (char) 230, (char) 231, (char) 232, (char) 233, (char) 234, (char) 235, (char) 236, (char) 237, (char) 238, (char) 239, (char) 240, (char) 241, (char) 242, (char) 243, (char) 244, (char) 245, (char) 246, (char) 247, (char) 248, (char) 249, (char) 250, (char) 251, (char) 252, (char) 253, (char) 254, (char) 255, (char) 256, (char) 257, (char) 258, (char) 259, (char) 260, (char) 261, (char) 262, (char) 263, (char) 264, (char) 265, (char) 266, (char) 267, (char) 268, (char) 269, (char) 270, (char) 271, (char) 272, (char) 273, (char) 274, (char) 275, (char) 276, (char) 277, (char) 278, (char) 279, (char) 280, (char) 281, (char) 282, (char) 283, (char) 284, (char) 285, (char) 286, (char) 287, (char) 288, (char) 289, (char) 290, (char) 291, (char) 292, (char) 293, (char) 294, (char) 295, (char) 296, (char) 297, (char) 298, (char) 299, (char) 300, (char) 301, (char) 302, (char) 303, (char) 304, (char) 305, (char) 306, (char) 307, (char) 308, (char) 309, (char) 310, (char) 311, (char) 312, (char) 313, (char) 314, (char) 315, (char) 316, (char) 317, (char) 318, (char) 319, (char) 320, (char) 321, (char) 322, (char) 323, (char) 324, (char) 325, (char) 326, (char) 327, (char) 328, (char) 329, (char) 330, (char) 331, (char) 332, (char) 333, (char) 334, (char) 335, (char) 336, (char) 337, (char) 338, (char) 339, (char) 340, (char) 341, (char) 342, (char) 343, (char) 344, (char) 345, (char) 346, (char) 347, (char) 348, (char) 349, (char) 350, (char) 351, (char) 352, (char) 353, (char) 354, (char) 355, (char) 356, (char) 357, (char) 358, (char) 359, (char) 360, (char) 361, (char) 362, (char) 363, (char) 364, (char) 365, (char) 366, (char) 367, (char) 368, (char) 369, (char) 370, (char) 371, (char) 372, (char) 373, (char) 374, (char) 375, (char) 376, (char) 377, (char) 378, (char) 379, (char) 380, (char) 381, (char) 382, (char) 383, (char) 384, (char) 385, (char) 386, (char) 387, (char) 388, (char) 389, (char) 390, (char) 391, (char) 392, (char) 393, (char) 394, (char) 395, (char) 396, (char) 397, (char) 398, (char) 399, (char) 400, (char) 401, (char) 402, (char) 403, (char) 404, (char) 405, (char) 406, (char) 407, (char) 408, (char) 409, (char) 410, (char) 411, (char) 412, (char) 413, (char) 414, (char) 415, (char) 416, (char) 417, (char) 418, (char) 419, (char) 420, (char) 421, (char) 422}), + }; + + private static readonly PhoneMap[] s_phoneMaps = DecompressPhoneMaps(s_phoneMapsCompressed); + + private static char[] s_updIds = new char[] { (char)1, (char)2, (char)3, (char)4, (char)33, (char)46, (char)97, (char)98, (char)99, (char)100, (char)101, (char)102, (char)103, (char)104, (char)105, (char)106, (char)107, (char)108, (char)109, (char)110, (char)111, (char)112, (char)113, (char)114, (char)115, (char)116, (char)117, (char)118, (char)119, (char)120, (char)121, (char)122, (char)124, (char)230, (char)231, (char)240, (char)248, (char)295, (char)331, (char)339, (char)448, (char)449, (char)450, (char)451, (char)592, (char)593, (char)594, (char)595, (char)596, (char)597, (char)598, (char)599, (char)600, (char)601, (char)602, (char)603, (char)604, (char)605, (char)606, (char)607, (char)608, (char)609, (char)610, (char)611, (char)612, (char)613, (char)614, (char)615, (char)616, (char)618, (char)619, (char)620, (char)621, (char)622, (char)623, (char)624, (char)625, (char)626, (char)627, (char)628, (char)629, (char)630, (char)632, (char)633, (char)634, (char)635, (char)637, (char)638, (char)640, (char)641, (char)642, (char)643, (char)644, (char)646, (char)647, (char)648, (char)649, (char)650, (char)651, (char)652, (char)653, (char)654, (char)655, (char)656, (char)657, (char)658, (char)659, (char)660, (char)661, (char)662, (char)663, (char)664, (char)665, (char)667, (char)668, (char)669, (char)671, (char)672, (char)673, (char)674, (char)675, (char)676, (char)677, (char)678, (char)679, (char)680, (char)688, (char)689, (char)690, (char)692, (char)695, (char)700, (char)712, (char)716, (char)720, (char)721, (char)728, (char)734, (char)736, (char)737, (char)740, (char)768, (char)769, (char)771, (char)772, (char)774, (char)776, (char)778, (char)779, (char)783, (char)792, (char)793, (char)794, (char)796, (char)797, (char)798, (char)799, (char)800, (char)804, (char)805, (char)809, (char)810, (char)812, (char)815, (char)816, (char)817, (char)820, (char)825, (char)826, (char)827, (char)828, (char)829, (char)865, (char)946, (char)952, (char)967, (char)1856, (char)8214, (char)8255, (char)8319, (char)8593, (char)8594, (char)8595, (char)8599, (char)8600 }; + + private static readonly PhonemeConverter s_upsConverter = new(s_phoneMaps[0]); + + #endregion + + #region Private Types + + private class PhoneMap + { + internal PhoneMap() { } + + internal int _lcid; + internal PhoneId[] _phoneIds; + } + + private class PhoneId : IComparer + { + internal PhoneId() { } + + internal string _phone; + internal char[] _cp; + + int IComparer.Compare(PhoneId x, PhoneId y) + { + return string.Compare(x._phone, y._phone, StringComparison.CurrentCulture); + } + } + + /// + /// Compressed version for the phone map so that the size for the pronunciation table is small in the dll. + /// A single large arrays of bytes (ASCII) is used to store the 'pron' string. Each string is zero terminated. + /// A single large array of char is used to store the code point for the 'pron' string. Each binary array for the pron by default + /// has a length of 1 character. If the length is greater than 1, then the 'pron' string is appended with -1 values, one per extra code + /// point. + /// + private class PhoneMapCompressed + { + internal PhoneMapCompressed() { } + + internal PhoneMapCompressed(int lcid, int count, byte[] phoneIds, char[] cps) + { + _lcid = lcid; + _count = count; + _phones = phoneIds; + _cps = cps; + } + + // Language Id + internal int _lcid; + + // Number of phonemes + internal int _count; + + // Array of zero terminated ASCII strings + internal byte[] _phones; + + // Array of code points for the 'pron'. By default each code point for a 'pron' is 1 char long, unless the 'pron' string is prepended with -1 + internal char[] _cps; + } + + #endregion + } +} diff --git a/src/libraries/System.Speech/src/Internal/RedBlackList.cs b/src/libraries/System.Speech/src/Internal/RedBlackList.cs new file mode 100644 index 00000000000000..65d7e114a82549 --- /dev/null +++ b/src/libraries/System.Speech/src/Internal/RedBlackList.cs @@ -0,0 +1,728 @@ +// 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; +using System.Collections.Generic; +using System.Diagnostics; +using System.Text; + +namespace System.Speech.Internal +{ + /// + /// Sorted List using the Red-Black algorithm + /// + internal abstract class RedBlackList : IEnumerable + { + #region Constructors + + internal RedBlackList() + { + } + + #endregion + + #region Internal Methods + + internal void Add(object key) + { +#if DEBUG + if (_root != null && _root._inEnumaration) + { + throw new InvalidOperationException(); + } +#endif + TreeNode node = new(key); + node.IsRed = true; + InsertNode(_root, node); + FixUpInsertion(node); + + _root = FindRoot(node); + } + + internal void Remove(object key) + { +#if DEBUG + if (_root != null && _root._inEnumaration) + { + throw new InvalidOperationException(); + } +#endif + TreeNode node = FindItem(_root, key); + if (node == null) + { + throw new KeyNotFoundException(); + } + TreeNode nodeRemoved = DeleteNode(node); + FixUpRemoval(nodeRemoved); + + if (nodeRemoved == _root) + { + if (_root.Left != null) + { + _root = FindRoot(_root.Left); + } + else if (_root.Right != null) + { + _root = FindRoot(_root.Right); + } + else + { + _root = null; + } + } + else + { + _root = FindRoot(_root); + } + } + + public IEnumerator GetEnumerator() + { + return new MyEnumerator(_root); + } + + #endregion + + #region Internal Properties + + internal bool IsEmpty + { + get + { + return _root == null; + } + } + + internal bool CountIsOne + { + get + { + return _root != null && _root.Left == null && _root.Right == null; + } + } + + internal bool ContainsMoreThanOneItem + { + get + { + return _root != null && (_root.Right != null || _root.Left != null); + } + } + + internal object First + { + get + { + if (_root == null) + { + // We don't expect First to be called on empty graphs + System.Diagnostics.Debug.Assert(false); + return null; + } + // Set the current pointer to the last element + return FindMinSubTree(_root).Key; + } + } + + #endregion + + #region Protected Methods + + protected abstract int CompareTo(object object1, object object2); + + #endregion + + #region Private Methods + + #region Implement utility operations on Tree + + private static TreeNode GetUncle(TreeNode node) + { + if (node.Parent == node.Parent.Parent.Left) + { + return node.Parent.Parent.Right; + } + else + { + return node.Parent.Parent.Left; + } + } + + private static TreeNode GetSibling(TreeNode node, TreeNode parent) + { + if (node == parent.Left) + { + return parent.Right; + } + else + { + return parent.Left; + } + } + + private static NodeColor GetColor(TreeNode node) + { + return node == null ? NodeColor.BLACK : (node.IsRed ? NodeColor.RED : NodeColor.BLACK); + } + + private static void SetColor(TreeNode node, NodeColor color) + { + if (node != null) + { + node.IsRed = (color == NodeColor.RED); + } + else + { + Debug.Assert(color == NodeColor.BLACK); + } + } + + private static void TakeParent(TreeNode node, TreeNode newNode) + { + if (node.Parent == null) + { + if (newNode != null) + { + newNode.Parent = null; + } + } + else if (node.Parent.Left == node) + { + node.Parent.Left = newNode; + } + else if (node.Parent.Right == node) + { + node.Parent.Right = newNode; + } + else + { + throw new InvalidOperationException(); + } + } + + private static TreeNode RotateLeft(TreeNode node) + { + TreeNode newNode = node.Right; + node.Right = newNode.Left; + TakeParent(node, newNode); + newNode.Left = node; + return newNode; + } + + private static TreeNode RotateRight(TreeNode node) + { + TreeNode newNode = node.Left; + node.Left = newNode.Right; + TakeParent(node, newNode); + newNode.Right = node; + + return newNode; + } + + private static TreeNode FindMinSubTree(TreeNode node) + { + while (node.Left != null) + { + node = node.Left; + } + return node; + } + + private static TreeNode FindSuccessor(TreeNode node) + { + if (node.Right == null) + { + while (node.Parent != null && node.Parent.Left != node) + { + node = node.Parent; + } + + return node.Parent == null ? null : node.Parent; + } + else + { + return FindMinSubTree(node.Right); + } + } + + // Return the actual node that is deleted + private static TreeNode DeleteNode(TreeNode node) + { + if (node.Right == null) + { + TakeParent(node, node.Left); + + return node; + } + else if (node.Left == null) + { + TakeParent(node, node.Right); + + return node; + } + else + { + TreeNode successor = FindSuccessor(node); + Debug.Assert(successor != null && successor.Left == null); + node.CopyNode(successor); + TakeParent(successor, successor.Right); + return successor; + } + } + + #endregion Implement utility operations on Tree + + // Return the root of the new subtree + private TreeNode InsertNode(TreeNode node, TreeNode newNode) + { + if (node == null) + { + return newNode; + } + + int diff = CompareTo(newNode.Key, node.Key); + + if (diff < 0) + { + node.Left = InsertNode(node.Left, newNode); + } + else + { + node.Right = InsertNode(node.Right, newNode); + } + + return node; + } + + private TreeNode FindItem(TreeNode node, object key) + { + if (node == null) + { + return null; + } + int diff = CompareTo(key, node.Key); + if (diff == 0) + { + return node; + } + else if (diff < 0) + { + return FindItem(node.Left, key); + } + else + { + return FindItem(node.Right, key); + } + } + + private TreeNode FindRoot(TreeNode node) + { + while (node.Parent != null) + { + node = node.Parent; + } + return node; + } + + private void FixUpInsertion(TreeNode node) + { + FixInsertCase1(node); + } + + private void FixInsertCase1(TreeNode node) + { + Debug.Assert(node.IsRed); + + if (node.Parent == null) + { + node.IsRed = false; + } + else + { + FixInsertCase2(node); + } + } + private void FixInsertCase2(TreeNode node) + { + if (GetColor(node.Parent) == NodeColor.BLACK) + { + return; // Tree is still valid. + } + + // Now, its parent is RED, so it must have an uncle since its parent is not root. + // Also, its grandparent must be BLACK. + Debug.Assert(GetColor(node.Parent.Parent) == NodeColor.BLACK); + TreeNode uncle = GetUncle(node); + + if (GetColor(uncle) == NodeColor.RED) + { + SetColor(node.Parent, NodeColor.BLACK); + SetColor(uncle, NodeColor.BLACK); + SetColor(node.Parent.Parent, NodeColor.RED); + FixInsertCase1(node.Parent.Parent); // Move recursively up + } + else + { + FixInsertCase3(node); + } + } + + private void FixInsertCase3(TreeNode node) + { + // + // Now it's RED, parent is RED, uncle is BLACK, + // We want to rotate so that its uncle is on the opposite side + if (node == node.Parent.Right && node.Parent == node.Parent.Parent.Left) + { + RotateLeft(node.Parent); + node = node.Left; + } + else if (node == node.Parent.Left && node.Parent == node.Parent.Parent.Right) + { + RotateRight(node.Parent); + node = node.Right; + } + FixInsertCase4(node); + } + + private void FixInsertCase4(TreeNode node) + { + // + // Must follow case 3, here we are finally done! + // + + SetColor(node.Parent, NodeColor.BLACK); + SetColor(node.Parent.Parent, NodeColor.RED); + if (node == node.Parent.Left) + { + Debug.Assert(node.Parent == node.Parent.Parent.Left); // From case 3 + RotateRight(node.Parent.Parent); + } + else + { + Debug.Assert(node.Parent == node.Parent.Parent.Right); // From case 3 + RotateLeft(node.Parent.Parent); + } + } + + private static void FixUpRemoval(TreeNode node) + { + // This node must have at most 1 child + Debug.Assert(node.Left == null || node.Right == null); + + TreeNode onlyChild = node.Left == null ? node.Right : node.Left; + + // This node should have been deleted already, and the child has replaced the this node. + Debug.Assert(node.Parent == null || node.Parent.Left == onlyChild || node.Parent.Right == onlyChild); + Debug.Assert(onlyChild == null || onlyChild.Parent == node.Parent); + + // + // If the node removed was red, all properties still hold. + // Otherwise, we need fix up. + // + + if (GetColor(node) == NodeColor.BLACK) + { + if (GetColor(onlyChild) == NodeColor.RED) + { + SetColor(onlyChild, NodeColor.BLACK); + } + else if (node.Parent == null) // if we remove a root node, nothing has changed. + { + return; + } + else + { + // + // Note that onlyChild could be null. + // The deleted node and its only child are BLACK, and there is a real parent, therefore, + // the total black height was at least 2 (excluding the real parent), thus the sibling subtree also has a black height of at least 2 + // + FixRemovalCase2(GetSibling(onlyChild, node.Parent)); + } + } + } + + private static void FixRemovalCase1(TreeNode node) + { + Debug.Assert(GetColor(node) == NodeColor.BLACK); + if (node.Parent == null) + { + return; + } + else + { + FixRemovalCase2(GetSibling(node, node.Parent)); + } + } + + private static void FixRemovalCase2(TreeNode sibling) + { + Debug.Assert(sibling != null); + if (GetColor(sibling) == NodeColor.RED) + { + Debug.Assert(sibling.Left != null && sibling.Right != null); + TreeNode parent = sibling.Parent; + // the parent must be black + SetColor(parent, NodeColor.RED); + SetColor(sibling, NodeColor.BLACK); + + if (sibling == parent.Right) + { + RotateLeft(sibling.Parent); + // new sibling was the old sibling left child, and must be non-leaf black + sibling = parent.Right; + } + else + { + RotateRight(sibling.Parent); + // new sibling was the old sibling right child, and must be non-leaf black + sibling = parent.Left; + } + } + + // Now the sibling will be a BLACK non-leaf. + FixRemovalCase3(sibling); + } + + private static void FixRemovalCase3(TreeNode sibling) + { + if (GetColor(sibling.Parent) == NodeColor.BLACK && + GetColor(sibling) == NodeColor.BLACK && + GetColor(sibling.Left) == NodeColor.BLACK && + GetColor(sibling.Right) == NodeColor.BLACK) + { + SetColor(sibling, NodeColor.RED); + FixRemovalCase1(sibling.Parent); + } + else + { + FixRemovalCase4(sibling); + } + } + + private static void FixRemovalCase4(TreeNode sibling) + { + if (GetColor(sibling.Parent) == NodeColor.RED && + GetColor(sibling) == NodeColor.BLACK && + GetColor(sibling.Left) == NodeColor.BLACK && + GetColor(sibling.Right) == NodeColor.BLACK) + { + SetColor(sibling, NodeColor.RED); + SetColor(sibling.Parent, NodeColor.BLACK); + } + else + { + FixRemovalCase5(sibling); + } + } + + private static void FixRemovalCase5(TreeNode sibling) + { + if (sibling == sibling.Parent.Right && + GetColor(sibling) == NodeColor.BLACK && + GetColor(sibling.Left) == NodeColor.RED && + GetColor(sibling.Right) == NodeColor.BLACK) + { + SetColor(sibling, NodeColor.RED); + SetColor(sibling.Left, NodeColor.BLACK); + RotateRight(sibling); + sibling = sibling.Parent; + } + else if (sibling == sibling.Parent.Left && + GetColor(sibling) == NodeColor.BLACK && + GetColor(sibling.Right) == NodeColor.RED && + GetColor(sibling.Left) == NodeColor.BLACK) + { + SetColor(sibling, NodeColor.RED); + SetColor(sibling.Right, NodeColor.BLACK); + RotateLeft(sibling); + sibling = sibling.Parent; + } + FixRemovalCase6(sibling); + } + + private static void FixRemovalCase6(TreeNode sibling) + { + Debug.Assert(GetColor(sibling) == NodeColor.BLACK); + + SetColor(sibling, GetColor(sibling.Parent)); + SetColor(sibling.Parent, NodeColor.BLACK); + if (sibling == sibling.Parent.Right) + { + Debug.Assert(GetColor(sibling.Right) == NodeColor.RED); + SetColor(sibling.Right, NodeColor.BLACK); + RotateLeft(sibling.Parent); + } + else + { + Debug.Assert(GetColor(sibling.Left) == NodeColor.RED); + SetColor(sibling.Left, NodeColor.BLACK); + RotateRight(sibling.Parent); + } + } + + #endregion + + #region Private Fields + + private TreeNode _root; + + #endregion + + #region Private Types + + private class MyEnumerator : IEnumerator + { + internal MyEnumerator(TreeNode node) + { + _root = node; + } + + public object Current + { + get + { + if (_node == null) + { + throw new InvalidOperationException(); + } + + return _node.Key; + } + } + + public bool MoveNext() + { + if (!_moved) + { + _node = _root != null ? FindMinSubTree(_root) : null; + _moved = true; +#if DEBUG + if (_root != null) + { + _root._inEnumaration = true; + } +#endif + } + else + { + _node = _node != null ? FindSuccessor(_node) : null; + } +#if DEBUG + if (_root != null) + { + _root._inEnumaration = _node != null; + } +#endif + return _node != null; + } + + public void Reset() + { + _moved = false; + _node = null; + } + + private TreeNode _node; + private TreeNode _root; + private bool _moved; + } + +#if DEBUG + [DebuggerDisplay("{((System.Speech.Internal.SrgsCompiler.Arc)Key).ToString ()}")] +#endif + private class TreeNode + { + internal TreeNode(object key) + { + _key = key; + } + + internal TreeNode Left + { + get + { + return _leftChild; + } + set + { + _leftChild = value; + if (_leftChild != null) + { + _leftChild._parent = this; + } + } + } + + internal TreeNode Right + { + get + { + return _rightChild; + } + set + { + _rightChild = value; + if (_rightChild != null) + { + _rightChild._parent = this; + } + } + } + + internal TreeNode Parent + { + get + { + return _parent; + } + set + { + _parent = value; + } + } + + internal bool IsRed + { + get + { + return _isRed; + } + set + { + _isRed = value; + } + } + + internal object Key + { + get + { + return _key; + } + } + + internal void CopyNode(TreeNode from) + { + _key = from._key; + } + +#if DEBUG + internal bool _inEnumaration; +#endif + private object _key; + private bool _isRed; + + private TreeNode _leftChild, _rightChild, _parent; + } + + private enum NodeColor + { + BLACK = 0, + RED = 1 + } + + #endregion + } +} diff --git a/src/libraries/System.Speech/src/Internal/ResourceLoader.cs b/src/libraries/System.Speech/src/Internal/ResourceLoader.cs new file mode 100644 index 00000000000000..f565249184be6d --- /dev/null +++ b/src/libraries/System.Speech/src/Internal/ResourceLoader.cs @@ -0,0 +1,114 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.IO; +using System.Net; + +namespace System.Speech.Internal +{ + internal class ResourceLoader + { + #region Internal Methods + + /// + /// Load a file either from a local network or from the Internet. + /// + internal Stream LoadFile(Uri uri, out string mimeType, out Uri baseUri, out string localPath) + { + localPath = null; + + { + Stream stream = null; + + // Check for a local file + if (!uri.IsAbsoluteUri || uri.IsFile) + { + // Local file + string file = uri.IsAbsoluteUri ? uri.LocalPath : uri.OriginalString; + try + { + stream = new FileStream(file, FileMode.Open, FileAccess.Read, FileShare.Read); + } + catch + { + if (Directory.Exists(file)) + { + throw new InvalidOperationException(SR.Get(SRID.CannotReadFromDirectory, file)); + } + throw; + } + baseUri = null; + } + else + { + try + { + // http:// Load the data from the web + stream = DownloadData(uri, out baseUri); + } + catch (WebException e) + { + throw new IOException(e.Message, e); + } + } + mimeType = null; + return stream; + } + } + + /// + /// Release a file from a cache if any + /// + internal void UnloadFile(string localPath) + { + } + + internal Stream LoadFile(Uri uri, out string localPath, out Uri redirectedUri) + { + string mediaTypeUnused; + return LoadFile(uri, out mediaTypeUnused, out redirectedUri, out localPath); + } + + #endregion + + #region Private Methods + + /// + /// Download data from the web. + /// Set the redirectUri as the location of the file could be redirected in ASP pages. + /// + private static Stream DownloadData(Uri uri, out Uri redirectedUri) + { +#pragma warning disable SYSLIB0014 + // Create a request for the URL. + WebRequest request = WebRequest.Create(uri); + + // If required by the server, set the credentials. + request.Credentials = CredentialCache.DefaultCredentials; + + // Get the response. + using (HttpWebResponse response = (HttpWebResponse)request.GetResponse()) + { + // Get the stream containing content returned by the server. + using (Stream dataStream = response.GetResponseStream()) + { + redirectedUri = response.ResponseUri; + + // http:// Load the data from the web + using (WebClient client = new()) + { + client.UseDefaultCredentials = true; + return new MemoryStream(client.DownloadData(redirectedUri)); + } + } + } +#pragma warning restore SYSLIB0014 + } + + #endregion + + #region Private Fields + + #endregion + } +} diff --git a/src/libraries/System.Speech/src/Internal/SapiAttributeParser.cs b/src/libraries/System.Speech/src/Internal/SapiAttributeParser.cs new file mode 100644 index 00000000000000..5052208312d78d --- /dev/null +++ b/src/libraries/System.Speech/src/Internal/SapiAttributeParser.cs @@ -0,0 +1,57 @@ +// 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.Globalization; +using System.Speech.AudioFormat; + +namespace System.Speech.Internal +{ + internal static class SapiAttributeParser + { + #region Internal Methods + + internal static CultureInfo GetCultureInfoFromLanguageString(string valueString) + { + string[] strings = valueString.Split(';'); + + string langStringTrim = strings[0].Trim(); + + if (!string.IsNullOrEmpty(langStringTrim)) + { + try + { + return new CultureInfo(int.Parse(langStringTrim, NumberStyles.HexNumber, CultureInfo.InvariantCulture), false); + } + catch (ArgumentException) + { + return null; // If we have an invalid language id ignore it. Otherwise enumerating recognizers or voices would fail. + } + } + + return null; + } + + internal static List GetAudioFormatsFromString(string valueString) + { + List formatList = new(); + string[] strings = valueString.Split(';'); + + for (int i = 0; i < strings.Length; i++) + { + string formatString = strings[i].Trim(); + if (!string.IsNullOrEmpty(formatString)) + { + SpeechAudioFormatInfo formatInfo = AudioFormatConverter.ToSpeechAudioFormatInfo(formatString); + if (formatInfo != null) // Skip cases where a Guid is used. + { + formatList.Add(formatInfo); + } + } + } + return formatList; + } + + #endregion + } +} diff --git a/src/libraries/System.Speech/src/Internal/SapiInterop/EventNotify.cs b/src/libraries/System.Speech/src/Internal/SapiInterop/EventNotify.cs new file mode 100644 index 00000000000000..d52a2b9bf69315 --- /dev/null +++ b/src/libraries/System.Speech/src/Internal/SapiInterop/EventNotify.cs @@ -0,0 +1,121 @@ +// 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.Speech.AudioFormat; +using System.Threading; + +namespace System.Speech.Internal.SapiInterop +{ + internal class SpNotifySink : ISpNotifySink + { + public SpNotifySink(EventNotify eventNotify) + { + _eventNotifyReference = new WeakReference(eventNotify); + } + + void ISpNotifySink.Notify() + { + EventNotify eventNotify = (EventNotify)_eventNotifyReference.Target; + if (eventNotify != null) + { + ThreadPool.QueueUserWorkItem(new WaitCallback(eventNotify.SendNotification)); + } + } + + private WeakReference _eventNotifyReference; + } + /// Dispatches events from ISpEventSource to DispatchEventDelegate on a thread + /// compatible with the application model of the thread that created this object. + internal class EventNotify + { + #region Constructors + + internal EventNotify(ISpEventSource sapiEventSource, IAsyncDispatch dispatcher, bool additionalSapiFeatures) + { + // Remember event source + _sapiEventSourceReference = new WeakReference(sapiEventSource); + + _dispatcher = dispatcher; + _additionalSapiFeatures = additionalSapiFeatures; + + // Start listening to events from sapiEventSource. + _notifySink = new SpNotifySink(this); + sapiEventSource.SetNotifySink(_notifySink); + } + + #endregion Constructors + + #region Internal Methods + + // Finalizer is not required since ISpEventSource and AsyncOperation both implement appropriate finalizers. + internal void Dispose() + { + lock (this) + { + // Since we are explicitly calling Dispose(), sapiEventSource (RCW) will normally be alive. + // If Dispose() is called from a finalizer this may not be the case so check for null. + if (_sapiEventSourceReference != null) + { + ISpEventSource sapiEventSource = (ISpEventSource)_sapiEventSourceReference.Target; + if (sapiEventSource != null) + { + // Stop listening to events from sapiEventSource. + sapiEventSource.SetNotifySink(null); + _notifySink = null; + } + } + _sapiEventSourceReference = null; + } + } + + internal void SendNotification(object ignored) + { + lock (this) + { + // Call dispatchEventDelegate for each SAPI event currently queued. + if (_sapiEventSourceReference != null) + { + ISpEventSource sapiEventSource = (ISpEventSource)_sapiEventSourceReference.Target; + if (sapiEventSource != null) + { + List speechEvents = new(); + SpeechEvent speechEvent; + while (null != (speechEvent = SpeechEvent.TryCreateSpeechEvent(sapiEventSource, _additionalSapiFeatures, _audioFormat))) + { + speechEvents.Add(speechEvent); + } + _dispatcher.Post(speechEvents.ToArray()); + } + } + } + } + + #endregion Methods + + #region Internal Properties + + internal SpeechAudioFormatInfo AudioFormat + { + set + { + _audioFormat = value; + } + } + + #endregion Methods + + #region Private Methods + + #endregion + + #region Private Fields + + private IAsyncDispatch _dispatcher; + private WeakReference _sapiEventSourceReference; + private bool _additionalSapiFeatures; + private SpeechAudioFormatInfo _audioFormat; + private ISpNotifySink _notifySink; + #endregion Private Fields + } +} diff --git a/src/libraries/System.Speech/src/Internal/SapiInterop/SapiEventInterop.cs b/src/libraries/System.Speech/src/Internal/SapiInterop/SapiEventInterop.cs new file mode 100644 index 00000000000000..610c9c508c020c --- /dev/null +++ b/src/libraries/System.Speech/src/Internal/SapiInterop/SapiEventInterop.cs @@ -0,0 +1,144 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Runtime.InteropServices; + +namespace System.Speech.Internal.SapiInterop +{ + [StructLayout(LayoutKind.Sequential)] + internal struct SPEVENT + { + public SPEVENTENUM eEventId; + public SPEVENTLPARAMTYPE elParamType; + public uint ulStreamNum; + public ulong ullAudioStreamOffset; + public IntPtr wParam; // Always just a numeric type - contains no unmanaged resources so does not need special clean-up. + public IntPtr lParam; // Can be a numeric type, or pointer to string or object. Use SafeSapiLParamHandle to cleanup. + } + + [StructLayout(LayoutKind.Sequential)] + internal struct SPEVENTEX + { + public SPEVENTENUM eEventId; + public SPEVENTLPARAMTYPE elParamType; + public uint ulStreamNum; + public ulong ullAudioStreamOffset; + public IntPtr wParam; // Always just a numeric type - contains no unmanaged resources so does not need special clean-up. + public IntPtr lParam; // Can be a numeric type, or pointer to string or object. Use SafeSapiLParamHandle to cleanup. + public ulong ullAudioTimeOffset; + } + + internal enum SPEVENTENUM : ushort + { + SPEI_UNDEFINED = 0, + + // TTS engine + SPEI_START_INPUT_STREAM = 1, + SPEI_END_INPUT_STREAM = 2, + SPEI_VOICE_CHANGE = 3, // LPARAM_IS_TOKEN + SPEI_TTS_BOOKMARK = 4, // LPARAM_IS_STRING + SPEI_WORD_BOUNDARY = 5, + SPEI_PHONEME = 6, + SPEI_SENTENCE_BOUNDARY = 7, + SPEI_VISEME = 8, + SPEI_TTS_AUDIO_LEVEL = 9, // wParam contains current output audio level + + // TTS engine vendors use these reserved bits + SPEI_TTS_PRIVATE = 15, + SPEI_MIN_TTS = 1, + SPEI_MAX_TTS = 15, + + // Speech Recognition + SPEI_END_SR_STREAM = 34, // LPARAM contains HRESULT, WPARAM contains flags (SPESF_xxx) + SPEI_SOUND_START = 35, + SPEI_SOUND_END = 36, + SPEI_PHRASE_START = 37, + SPEI_RECOGNITION = 38, + SPEI_HYPOTHESIS = 39, + SPEI_SR_BOOKMARK = 40, + SPEI_PROPERTY_NUM_CHANGE = 41, // LPARAM points to a string, WPARAM is the attrib value + SPEI_PROPERTY_STRING_CHANGE = 42, // LPARAM pointer to buffer. Two concatenated null terminated strings. + SPEI_FALSE_RECOGNITION = 43, // apparent speech with no valid recognition + SPEI_INTERFERENCE = 44, // LPARAM is any combination of SPINTERFERENCE flags + SPEI_REQUEST_UI = 45, // LPARAM is string. + SPEI_RECO_STATE_CHANGE = 46, // wParam contains new reco state + SPEI_ADAPTATION = 47, // we are now ready to accept the adaptation buffer + SPEI_START_SR_STREAM = 48, + SPEI_RECO_OTHER_CONTEXT = 49, // Phrase finished and recognized, but for other context + SPEI_SR_AUDIO_LEVEL = 50, // wParam contains current input audio level + SPEI_SR_RETAINEDAUDIO = 51, + SPEI_SR_PRIVATE = 52, + SPEI_ACTIVE_CATEGORY_CHANGED = 53, // LPARAM is a pointer to the new active category + SPEI_TEXTFEEDBACK = 54, // LPARAM is a pointer to FILETIME + FeedbackText + SPEI_RECOGNITION_ALL = 55, + SPEI_BARGE_IN = 56, + + // SPEI_MIN_SR = 34, + // SPEI_MAX_SR = 56, + SPEI_RESERVED1 = 30, // do not use + SPEI_RESERVED2 = 33, // do not use + SPEI_RESERVED3 = 63 // do not use + } + + [ComImport, Guid("5EFF4AEF-8487-11D2-961C-00C04F8EE628"), InterfaceType(ComInterfaceType.InterfaceIsIUnknown)] + internal interface ISpNotifySource + { + // ISpNotifySource Methods + void SetNotifySink(ISpNotifySink pNotifySink); + void SetNotifyWindowMessage(uint hWnd, uint Msg, IntPtr wParam, IntPtr lParam); + void Slot3(); // void SetNotifyCallbackFunction(ref IntPtr pfnCallback, IntPtr wParam, IntPtr lParam); + void Slot4(); // void SetNotifyCallbackInterface(ref IntPtr pSpCallback, IntPtr wParam, IntPtr lParam); + void Slot5(); // void SetNotifyWin32Event(); + [PreserveSig] + int WaitForNotifyEvent(uint dwMilliseconds); + void Slot7(); // IntPtr GetNotifyEventHandle(); + } + + [ComImport, Guid("BE7A9CCE-5F9E-11D2-960F-00C04F8EE628"), InterfaceType(ComInterfaceType.InterfaceIsIUnknown)] + internal interface ISpEventSource : ISpNotifySource + { + // ISpNotifySource Methods + new void SetNotifySink(ISpNotifySink pNotifySink); + new void SetNotifyWindowMessage(uint hWnd, uint Msg, IntPtr wParam, IntPtr lParam); + new void Slot3(); // void SetNotifyCallbackFunction(ref IntPtr pfnCallback, IntPtr wParam, IntPtr lParam); + new void Slot4(); // void SetNotifyCallbackInterface(ref IntPtr pSpCallback, IntPtr wParam, IntPtr lParam); + new void Slot5(); // void SetNotifyWin32Event(); + [PreserveSig] + new int WaitForNotifyEvent(uint dwMilliseconds); + new void Slot7(); // IntPtr GetNotifyEventHandle(); + + // ISpEventSource Methods + void SetInterest(ulong ullEventInterest, ulong ullQueuedInterest); + void GetEvents(uint ulCount, out SPEVENT pEventArray, out uint pulFetched); + void Slot10(); // void GetInfo(out SPEVENTSOURCEINFO pInfo); + } + + [ComImport, Guid("2373A435-6A4B-429e-A6AC-D4231A61975B"), InterfaceType(ComInterfaceType.InterfaceIsIUnknown)] + internal interface ISpEventSource2 : ISpEventSource + { + // ISpNotifySource Methods + new void SetNotifySink(ISpNotifySink pNotifySink); + new void SetNotifyWindowMessage(uint hWnd, uint Msg, IntPtr wParam, IntPtr lParam); + new void Slot3(); // void SetNotifyCallbackFunction(ref IntPtr pfnCallback, IntPtr wParam, IntPtr lParam); + new void Slot4(); // void SetNotifyCallbackInterface(ref IntPtr pSpCallback, IntPtr wParam, IntPtr lParam); + new void Slot5(); // void SetNotifyWin32Event(); + [PreserveSig] + new int WaitForNotifyEvent(uint dwMilliseconds); + new void Slot7(); // IntPtr GetNotifyEventHandle(); + + // ISpEventSource Methods + new void SetInterest(ulong ullEventInterest, ulong ullQueuedInterest); + new void GetEvents(uint ulCount, out SPEVENT pEventArray, out uint pulFetched); + new void Slot10(); // void GetInfo(out SPEVENTSOURCEINFO pInfo); + + // ISpEventSource2 Methods + void GetEventsEx(uint ulCount, out SPEVENTEX pEventArray, out uint pulFetched); + } + + [ComImport, Guid("259684DC-37C3-11D2-9603-00C04F8EE628"), InterfaceType(ComInterfaceType.InterfaceIsIUnknown)] + internal interface ISpNotifySink + { + // ISpNotifySink Methods + void Notify(); + } +} diff --git a/src/libraries/System.Speech/src/Internal/SapiInterop/SapiGrammar.cs b/src/libraries/System.Speech/src/Internal/SapiInterop/SapiGrammar.cs new file mode 100644 index 00000000000000..290af3331d561f --- /dev/null +++ b/src/libraries/System.Speech/src/Internal/SapiInterop/SapiGrammar.cs @@ -0,0 +1,118 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Runtime.InteropServices; + +namespace System.Speech.Internal.SapiInterop +{ + internal class SapiGrammar : IDisposable + { + #region Constructors + + internal SapiGrammar(ISpRecoGrammar sapiGrammar, SapiProxy thread) + { + _sapiGrammar = sapiGrammar; + _sapiProxy = thread; + } + + public void Dispose() + { + if (!_disposed) + { + Marshal.ReleaseComObject(_sapiGrammar); + GC.SuppressFinalize(this); + _disposed = true; + } + } + + #endregion + + #region Internal Methods + + internal void SetGrammarState(SPGRAMMARSTATE state) + { + _sapiProxy.Invoke2(delegate { _sapiGrammar.SetGrammarState(state); }); + } + + internal void SetWordSequenceData(string text, SPTEXTSELECTIONINFO info) + { + SPTEXTSELECTIONINFO selectionInfo = info; + _sapiProxy.Invoke2(delegate { _sapiGrammar.SetWordSequenceData(text, (uint)text.Length, ref selectionInfo); }); + } + + internal void LoadCmdFromMemory(IntPtr grammar, SPLOADOPTIONS options) + { + _sapiProxy.Invoke2(delegate { _sapiGrammar.LoadCmdFromMemory(grammar, options); }); + } + + internal void LoadDictation(string pszTopicName, SPLOADOPTIONS options) + { + _sapiProxy.Invoke2(delegate { _sapiGrammar.LoadDictation(pszTopicName, options); }); + } + + internal SAPIErrorCodes SetDictationState(SPRULESTATE state) + { + return (SAPIErrorCodes)_sapiProxy.Invoke(delegate { return _sapiGrammar.SetDictationState(state); }); + } + + internal SAPIErrorCodes SetRuleState(string name, SPRULESTATE state) + { + return (SAPIErrorCodes)_sapiProxy.Invoke(delegate { return _sapiGrammar.SetRuleState(name, IntPtr.Zero, state); }); + } + + /* + * The Set of methods are only available with SAPI 5.3. There is no need then to use the SAPI proxy to switch + * the call to an MTA thread. + * + */ + internal void SetGrammarLoader(ISpGrammarResourceLoader resourceLoader) + { + SpRecoGrammar2.SetGrammarLoader(resourceLoader); + } + + internal void LoadCmdFromMemory2(IntPtr grammar, SPLOADOPTIONS options, string sharingUri, string baseUri) + { + SpRecoGrammar2.LoadCmdFromMemory2(grammar, options, sharingUri, baseUri); + } + + internal void SetRulePriority(string name, uint id, int priority) + { + SpRecoGrammar2.SetRulePriority(name, id, priority); + } + internal void SetRuleWeight(string name, uint id, float weight) + { + SpRecoGrammar2.SetRuleWeight(name, id, weight); + } + internal void SetDictationWeight(float weight) + { + SpRecoGrammar2.SetDictationWeight(weight); + } + + #endregion + + #region Internal Properties + + internal ISpRecoGrammar2 SpRecoGrammar2 + { + get + { + if (_sapiGrammar2 == null) + { + _sapiGrammar2 = (ISpRecoGrammar2)_sapiGrammar; + } + return _sapiGrammar2; + } + } + + #endregion + + #region Private Methods + + private ISpRecoGrammar2 _sapiGrammar2; + private ISpRecoGrammar _sapiGrammar; + private SapiProxy _sapiProxy; + private bool _disposed; + + #endregion + } +} diff --git a/src/libraries/System.Speech/src/Internal/SapiInterop/SapiInterop.cs b/src/libraries/System.Speech/src/Internal/SapiInterop/SapiInterop.cs new file mode 100644 index 00000000000000..c3968995caf1f1 --- /dev/null +++ b/src/libraries/System.Speech/src/Internal/SapiInterop/SapiInterop.cs @@ -0,0 +1,287 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Runtime.InteropServices; +using Microsoft.Win32.SafeHandles; + +namespace System.Speech.Internal.SapiInterop +{ + #region Enum + + // See sperror.h + internal enum SAPIErrorCodes + { + S_OK = 0, // 0x00000000 + S_FALSE = 1, // 0x00000001 + SP_NO_RULE_ACTIVE = 0x00045055, + SP_NO_RULES_TO_ACTIVATE = 282747, // 0x0004507B + + S_LIMIT_REACHED = 0x0004507F, + + E_FAIL = -2147467259, // 0x80004005 + SP_NO_PARSE_FOUND = 0x0004502c, + SP_WORD_EXISTS_WITHOUT_PRONUNCIATION = 0x00045037, // 282679 + + SPERR_FIRST = -2147201023, // 0x80045001 + SPERR_LAST = -2147200890, // 0x80045086 + + STG_E_FILENOTFOUND = -2147287038, // 0x80030002 + CLASS_E_CLASSNOTAVAILABLE = -2147221231, // 0x80040111 + REGDB_E_CLASSNOTREG = -2147221164, // 0x80040154 + SPERR_UNSUPPORTED_FORMAT = -2147201021, // 0x80045003 + SPERR_UNSUPPORTED_PHONEME = -2147200902, // 0x8004507A + SPERR_VOICE_NOT_FOUND = -2147200877, // 0x80045093 + SPERR_NOT_IN_LEX = -2147200999, // 0x80045019 + SPERR_TOO_MANY_GRAMMARS = -2147200990, // 0x80045022 + SPERR_INVALID_IMPORT = -2147200988, // 0x80045024 + SPERR_STREAM_CLOSED = -2147200968, // 0x80045038 + SPERR_NO_MORE_ITEMS = -2147200967, // 0x80045039 + SPERR_NOT_FOUND = -2147200966, // 0x8004503A + SPERR_NOT_TOPLEVEL_RULE = -2147200940, // 0x80045054 + SPERR_SHARED_ENGINE_DISABLED = -2147200906, // 0x80045076 + SPERR_RECOGNIZER_NOT_FOUND = -2147200905, // 0x80045077 + SPERR_AUDIO_NOT_FOUND = -2147200904, // 0x80045078 + SPERR_NOT_SUPPORTED_FOR_INPROC_RECOGNIZER = -2147200893, // 0x80045083 + SPERR_LEX_INVALID_DATA = -2147200891, // 0x80045085 + SPERR_CFG_INVALID_DATA = -2147200890 // 0x80045086 + } + + #endregion Enum + + #region SAPI constants + + internal static class SapiConstants + { + internal const string SPPROP_RESPONSE_SPEED = "ResponseSpeed"; + internal const string SPPROP_COMPLEX_RESPONSE_SPEED = "ComplexResponseSpeed"; + internal const string SPPROP_CFG_CONFIDENCE_REJECTION_THRESHOLD = "CFGConfidenceRejectionThreshold"; + + internal const uint SPDF_ALL = 0xff; + + // Throws exception if the specified Rule does not have a valid Id. + internal static SRID SapiErrorCode2SRID(SAPIErrorCodes code) + { + if (code >= SAPIErrorCodes.SPERR_FIRST && code <= SAPIErrorCodes.SPERR_LAST) + { + return (SRID)((int)SRID.SapiErrorUninitialized + (code - SAPIErrorCodes.SPERR_FIRST)); + } + else + { + switch (code) + { + case SAPIErrorCodes.SP_NO_RULE_ACTIVE: + return SRID.SapiErrorNoRuleActive; + + case SAPIErrorCodes.SP_NO_RULES_TO_ACTIVATE: + return SRID.SapiErrorNoRulesToActivate; + + case SAPIErrorCodes.SP_NO_PARSE_FOUND: + return SRID.NoParseFound; + + case SAPIErrorCodes.S_FALSE: + return SRID.UnexpectedError; + + default: + return (SRID)unchecked(-1); + } + } + } + } + + #endregion + + #region Interface + + [ComImport, Guid("14056589-E16C-11D2-BB90-00C04F8EE6C0"), InterfaceType(ComInterfaceType.InterfaceIsIUnknown)] + internal interface ISpObjectToken : ISpDataKey + { + // ISpDataKey Methods + [PreserveSig] + new int SetData([MarshalAs(UnmanagedType.LPWStr)] string pszValueName, uint cbData, [MarshalAs(UnmanagedType.LPArray, SizeParamIndex = 1)] byte[] pData); + [PreserveSig] + new int GetData([MarshalAs(UnmanagedType.LPWStr)] string pszValueName, ref uint pcbData, [MarshalAs(UnmanagedType.LPArray, SizeParamIndex = 1), Out] byte[] pData); + [PreserveSig] + new int SetStringValue([MarshalAs(UnmanagedType.LPWStr)] string pszValueName, [MarshalAs(UnmanagedType.LPWStr)] string pszValue); + [PreserveSig] + new int GetStringValue([MarshalAs(UnmanagedType.LPWStr)] string pszValueName, [MarshalAs(UnmanagedType.LPWStr)] out string ppszValue); + [PreserveSig] + new int SetDWORD([MarshalAs(UnmanagedType.LPWStr)] string pszValueName, uint dwValue); + [PreserveSig] + new int GetDWORD([MarshalAs(UnmanagedType.LPWStr)] string pszValueName, ref uint pdwValue); + [PreserveSig] + new int OpenKey([MarshalAs(UnmanagedType.LPWStr)] string pszSubKeyName, out ISpDataKey ppSubKey); + [PreserveSig] + new int CreateKey([MarshalAs(UnmanagedType.LPWStr)] string pszSubKey, out ISpDataKey ppSubKey); + [PreserveSig] + new int DeleteKey([MarshalAs(UnmanagedType.LPWStr)] string pszSubKey); + [PreserveSig] + new int DeleteValue([MarshalAs(UnmanagedType.LPWStr)] string pszValueName); + [PreserveSig] + new int EnumKeys(uint Index, [MarshalAs(UnmanagedType.LPWStr)] out string ppszSubKeyName); + [PreserveSig] + new int EnumValues(uint Index, [MarshalAs(UnmanagedType.LPWStr)] out string ppszValueName); + + // ISpObjectToken Methods + void SetId([MarshalAs(UnmanagedType.LPWStr)] string pszCategoryId, [MarshalAs(UnmanagedType.LPWStr)] string pszTokenId, [MarshalAs(UnmanagedType.Bool)] bool fCreateIfNotExist); + void GetId(out IntPtr ppszCoMemTokenId); + void Slot15(); // void GetCategory(out ISpObjectTokenCategory ppTokenCategory); + void Slot16(); // void CreateInstance(object pUnkOuter, UInt32 dwClsContext, ref Guid riid, ref IntPtr ppvObject); + void Slot17(); // void GetStorageFileName(ref Guid clsidCaller, [MarshalAs(UnmanagedType.LPWStr)] string pszValueName, [MarshalAs(UnmanagedType.LPWStr)] string pszFileNameSpecifier, UInt32 nFolder, [MarshalAs(UnmanagedType.LPWStr)] out string ppszFilePath); + void Slot18(); // void RemoveStorageFileName(ref Guid clsidCaller, [MarshalAs(UnmanagedType.LPWStr)] string pszKeyName, int fDeleteFile); + void Slot19(); // void Remove(ref Guid pclsidCaller); + void Slot20(); // void IsUISupported([MarshalAs(UnmanagedType.LPWStr)] string pszTypeOfUI, IntPtr pvExtraData, UInt32 cbExtraData, object punkObject, ref Int32 pfSupported); + void Slot21(); // void DisplayUI(UInt32 hWndParent, [MarshalAs(UnmanagedType.LPWStr)] string pszTitle, [MarshalAs(UnmanagedType.LPWStr)] string pszTypeOfUI, IntPtr pvExtraData, UInt32 cbExtraData, object punkObject); + void MatchesAttributes([MarshalAs(UnmanagedType.LPWStr)] string pszAttributes, [MarshalAs(UnmanagedType.Bool)] out bool pfMatches); + } + + //--- ISpObjectWithToken ---------------------------------------------------- + [ComImport, Guid("5B559F40-E952-11D2-BB91-00C04F8EE6C0"), InterfaceType(ComInterfaceType.InterfaceIsIUnknown)] + internal interface ISpObjectWithToken + { + [PreserveSig] + int SetObjectToken(ISpObjectToken pToken); + [PreserveSig] + int GetObjectToken(out ISpObjectToken ppToken); + }; + + [ComImport, Guid("14056581-E16C-11D2-BB90-00C04F8EE6C0"), InterfaceType(ComInterfaceType.InterfaceIsIUnknown)] + internal interface ISpDataKey + { + // ISpDataKey Methods + [PreserveSig] + int SetData([MarshalAs(UnmanagedType.LPWStr)] string valueName, uint cbData, [MarshalAs(UnmanagedType.LPArray, SizeParamIndex = 1)] byte[] data); + [PreserveSig] + int GetData([MarshalAs(UnmanagedType.LPWStr)] string valueName, ref uint pcbData, [MarshalAs(UnmanagedType.LPArray, SizeParamIndex = 1), Out] byte[] data); + [PreserveSig] + int SetStringValue([MarshalAs(UnmanagedType.LPWStr)] string valueName, [MarshalAs(UnmanagedType.LPWStr)] string value); + [PreserveSig] + int GetStringValue([MarshalAs(UnmanagedType.LPWStr)] string valueName, [MarshalAs(UnmanagedType.LPWStr)] out string value); + [PreserveSig] + int SetDWORD([MarshalAs(UnmanagedType.LPWStr)] string valueName, uint dwValue); + [PreserveSig] + int GetDWORD([MarshalAs(UnmanagedType.LPWStr)] string valueName, ref uint pdwValue); + [PreserveSig] + int OpenKey([MarshalAs(UnmanagedType.LPWStr)] string subKeyName, out ISpDataKey ppSubKey); + [PreserveSig] + int CreateKey([MarshalAs(UnmanagedType.LPWStr)] string subKey, out ISpDataKey ppSubKey); + [PreserveSig] + int DeleteKey([MarshalAs(UnmanagedType.LPWStr)] string subKey); + [PreserveSig] + int DeleteValue([MarshalAs(UnmanagedType.LPWStr)] string valueName); + [PreserveSig] + int EnumKeys(uint index, [MarshalAs(UnmanagedType.LPWStr)] out string ppszSubKeyName); + [PreserveSig] + int EnumValues(uint index, [MarshalAs(UnmanagedType.LPWStr)] out string valueName); + } + + [ComImport, Guid("92A66E2B-C830-4149-83DF-6FC2BA1E7A5B"), InterfaceType(ComInterfaceType.InterfaceIsIUnknown)] + internal interface ISpRegDataKey : ISpDataKey + { + // ISpDataKey Methods + [PreserveSig] + new int SetData([MarshalAs(UnmanagedType.LPWStr)] string valueName, uint cbData, [MarshalAs(UnmanagedType.LPArray, SizeParamIndex = 1)] byte[] data); + [PreserveSig] + new int GetData([MarshalAs(UnmanagedType.LPWStr)] string valueName, ref uint pcbData, [MarshalAs(UnmanagedType.LPArray, SizeParamIndex = 1), Out] byte[] data); + [PreserveSig] + new int SetStringValue([MarshalAs(UnmanagedType.LPWStr)] string valueName, [MarshalAs(UnmanagedType.LPWStr)] string value); + [PreserveSig] + new int GetStringValue([MarshalAs(UnmanagedType.LPWStr)] string pszValueName, [MarshalAs(UnmanagedType.LPWStr)] out string ppszValue); + [PreserveSig] + new int SetDWORD([MarshalAs(UnmanagedType.LPWStr)] string valueName, uint dwValue); + [PreserveSig] + new int GetDWORD([MarshalAs(UnmanagedType.LPWStr)] string pszValueName, ref uint pdwValue); + [PreserveSig] + new int OpenKey([MarshalAs(UnmanagedType.LPWStr)] string pszSubKeyName, out ISpDataKey ppSubKey); + [PreserveSig] + new int CreateKey([MarshalAs(UnmanagedType.LPWStr)] string subKey, out ISpDataKey ppSubKey); + [PreserveSig] + new int DeleteKey([MarshalAs(UnmanagedType.LPWStr)] string subKey); + [PreserveSig] + new int DeleteValue([MarshalAs(UnmanagedType.LPWStr)] string valueName); + [PreserveSig] + new int EnumKeys(uint index, [MarshalAs(UnmanagedType.LPWStr)] out string ppszSubKeyName); + [PreserveSig] + new int EnumValues(uint Index, [MarshalAs(UnmanagedType.LPWStr)] out string ppszValueName); + + // ISpRegDataKey Method + [PreserveSig] + int SetKey(SafeRegistryHandle hkey, bool fReadOnly); + } + + [ComImport, Guid("2D3D3845-39AF-4850-BBF9-40B49780011D"), InterfaceType(ComInterfaceType.InterfaceIsIUnknown)] + internal interface ISpObjectTokenCategory : ISpDataKey + { + // ISpDataKey Methods + [PreserveSig] + new int SetData([MarshalAs(UnmanagedType.LPWStr)] string valueName, uint cbData, [MarshalAs(UnmanagedType.LPArray, SizeParamIndex = 1)] byte[] data); + [PreserveSig] + new int GetData([MarshalAs(UnmanagedType.LPWStr)] string valueName, ref uint pcbData, [MarshalAs(UnmanagedType.LPArray, SizeParamIndex = 1), Out] byte[] data); + [PreserveSig] + new int SetStringValue([MarshalAs(UnmanagedType.LPWStr)] string valueName, [MarshalAs(UnmanagedType.LPWStr)] string value); + [PreserveSig] + new void GetStringValue([MarshalAs(UnmanagedType.LPWStr)] string pszValueName, [MarshalAs(UnmanagedType.LPWStr)] out string ppszValue); + [PreserveSig] + new int SetDWORD([MarshalAs(UnmanagedType.LPWStr)] string valueName, uint dwValue); + [PreserveSig] + new int GetDWORD([MarshalAs(UnmanagedType.LPWStr)] string pszValueName, ref uint pdwValue); + [PreserveSig] + new int OpenKey([MarshalAs(UnmanagedType.LPWStr)] string pszSubKeyName, out ISpDataKey ppSubKey); + [PreserveSig] + new int CreateKey([MarshalAs(UnmanagedType.LPWStr)] string subKey, out ISpDataKey ppSubKey); + [PreserveSig] + new int DeleteKey([MarshalAs(UnmanagedType.LPWStr)] string subKey); + [PreserveSig] + new int DeleteValue([MarshalAs(UnmanagedType.LPWStr)] string valueName); + [PreserveSig] + new int EnumKeys(uint index, [MarshalAs(UnmanagedType.LPWStr)] out string ppszSubKeyName); + [PreserveSig] + new int EnumValues(uint Index, [MarshalAs(UnmanagedType.LPWStr)] out string ppszValueName); + + // ISpObjectTokenCategory Methods + void SetId([MarshalAs(UnmanagedType.LPWStr)] string pszCategoryId, [MarshalAs(UnmanagedType.Bool)] bool fCreateIfNotExist); + void GetId([MarshalAs(UnmanagedType.LPWStr)] out string ppszCoMemCategoryId); + void Slot14(); // void GetDataKey(System.Speech.Internal.SPDATAKEYLOCATION spdkl, out ISpDataKey ppDataKey); + void EnumTokens([MarshalAs(UnmanagedType.LPWStr)] string pzsReqAttribs, [MarshalAs(UnmanagedType.LPWStr)] string pszOptAttribs, out IEnumSpObjectTokens ppEnum); + void Slot16(); // void SetDefaultTokenId([MarshalAs(UnmanagedType.LPWStr)] string pszTokenId); + void GetDefaultTokenId([MarshalAs(UnmanagedType.LPWStr)] out string ppszCoMemTokenId); + } + + [ComImport, Guid("06B64F9E-7FDA-11D2-B4F2-00C04F797396"), InterfaceType(ComInterfaceType.InterfaceIsIUnknown)] + internal interface IEnumSpObjectTokens + { + void Slot1(); // void Next(UInt32 celt, [MarshalAs(UnmanagedType.LPArray, SizeParamIndex = 0), Out] ISpObjectToken[] pelt, out UInt32 pceltFetched); + void Slot2(); // void Skip(UInt32 celt); + void Slot3(); // void Reset(); + void Slot4(); // void Clone(out IEnumSpObjectTokens ppEnum); + void Item(uint Index, out ISpObjectToken ppToken); + void GetCount(out uint pCount); + } + + [ComImport, Guid("B2745EFD-42CE-48CA-81F1-A96E02538A90"), InterfaceType(ComInterfaceType.InterfaceIsIUnknown)] + internal interface ISpPhoneticAlphabetSelection + { + void IsAlphabetUPS([MarshalAs(UnmanagedType.Bool)] out bool pfIsUPS); + void SetAlphabetToUPS([MarshalAs(UnmanagedType.Bool)] bool fForceUPS); + } + + [ComImport, Guid("EF411752-3736-4CB4-9C8C-8EF4CCB58EFE")] + internal class SpObjectToken { } + + [ComImport, Guid("A910187F-0C7A-45AC-92CC-59EDAFB77B53")] + internal class SpObjectTokenCategory { } + + [ComImport, Guid("D9F6EE60-58C9-458B-88E1-2F908FD7F87C")] + internal class SpDataKey { } + + #endregion + + #region Utility Class + + internal static class SAPIGuids + { + internal static readonly Guid SPDFID_WaveFormatEx = new("C31ADBAE-527F-4ff5-A230-F62BB61FF70C"); + internal static readonly Guid SPDFID_Text = new("7CEEF9F9-3D13-11d2-9EE7-00C04F797396"); + } + + #endregion +} diff --git a/src/libraries/System.Speech/src/Internal/SapiInterop/SapiProxy.cs b/src/libraries/System.Speech/src/Internal/SapiInterop/SapiProxy.cs new file mode 100644 index 00000000000000..c7a72696382895 --- /dev/null +++ b/src/libraries/System.Speech/src/Internal/SapiInterop/SapiProxy.cs @@ -0,0 +1,278 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Runtime.ExceptionServices; +using System.Runtime.InteropServices; +using System.Threading; + +namespace System.Speech.Internal.SapiInterop +{ + internal abstract class SapiProxy : IDisposable + { + #region Constructors + + public virtual void Dispose() + { + GC.SuppressFinalize(this); + } + + #endregion + + #region Internal Methods + + internal abstract object Invoke(ObjectDelegate pfn); + internal abstract void Invoke2(VoidDelegate pfn); + + #endregion + + #region Internal Properties + + internal ISpRecognizer Recognizer + { + get + { + return _recognizer; + } + } + + internal ISpRecognizer2 Recognizer2 + { + get + { + if (_recognizer2 == null) + { + _recognizer2 = (ISpRecognizer2)_recognizer; + } + return _recognizer2; + } + } + + internal ISpeechRecognizer SapiSpeechRecognizer + { + get + { + if (_speechRecognizer == null) + { + _speechRecognizer = (ISpeechRecognizer)_recognizer; + } + return _speechRecognizer; + } + } + + #endregion + + #region Protected Fields + + protected ISpeechRecognizer _speechRecognizer; + protected ISpRecognizer2 _recognizer2; + protected ISpRecognizer _recognizer; + + #endregion + + #region Protected Fields + + internal class PassThrough : SapiProxy, IDisposable + { + #region Constructors + + internal PassThrough(ISpRecognizer recognizer) + { + _recognizer = recognizer; + } + + ~PassThrough() + { + Dispose(false); + } + public override void Dispose() + { + try + { + Dispose(true); + } + finally + { + base.Dispose(); + } + } + + #endregion + + #region Internal Methods + + internal override object Invoke(ObjectDelegate pfn) + { + return pfn.Invoke(); + } + + internal override void Invoke2(VoidDelegate pfn) + { + pfn.Invoke(); + } + + #endregion + + #region Private Methods + + private void Dispose(bool disposing) + { + _recognizer2 = null; + _speechRecognizer = null; + Marshal.ReleaseComObject(_recognizer); + } + + #endregion + } + +#pragma warning disable 56500 // Remove all the catch all statements warnings used by the interop layer + + internal class MTAThread : SapiProxy, IDisposable + { + #region Constructors + + internal MTAThread(SapiRecognizer.RecognizerType type) + { + _mta = new Thread(new ThreadStart(SapiMTAThread)); + if (!_mta.TrySetApartmentState(ApartmentState.MTA)) + { + throw new InvalidOperationException(); + } + _mta.IsBackground = true; + _mta.Start(); + + if (type == SapiRecognizer.RecognizerType.InProc) + { + Invoke2(delegate { _recognizer = (ISpRecognizer)new SpInprocRecognizer(); }); + } + else + { + Invoke2(delegate { _recognizer = (ISpRecognizer)new SpSharedRecognizer(); }); + } + } + + ~MTAThread() + { + Dispose(false); + } + + public override void Dispose() + { + try + { + Dispose(true); + } + finally + { + base.Dispose(); + } + } + + #endregion + + #region Internal Methods + + internal override object Invoke(ObjectDelegate pfn) + { + lock (this) + { + _doit = pfn; + _process.Set(); + _done.WaitOne(); + if (_exception == null) + { + return _result; + } + else + { + ExceptionDispatchInfo.Throw(_exception); + return null; + } + } + } + + internal override void Invoke2(VoidDelegate pfn) + { + lock (this) + { + _doit2 = pfn; + _process.Set(); + _done.WaitOne(); + if (_exception != null) + { + ExceptionDispatchInfo.Throw(_exception); + } + } + } + + #endregion + + #region Private Methods + + private void Dispose(bool disposing) + { + lock (this) + { + _recognizer2 = null; + _speechRecognizer = null; + Invoke2(delegate { Marshal.ReleaseComObject(_recognizer); }); + ((IDisposable)_process).Dispose(); + ((IDisposable)_done).Dispose(); + } + base.Dispose(); + } + + private void SapiMTAThread() + { + while (true) + { + try + { + _process.WaitOne(); + _exception = null; + if (_doit != null) + { + _result = _doit.Invoke(); + _doit = null; + } + else + { + _doit2.Invoke(); + _doit2 = null; + } + } + catch (Exception e) + { + _exception = e; + } + try + { + _done.Set(); + } + catch (ObjectDisposedException) + { + break; + } + } + } + + #endregion + + #region Private Fields + + private Thread _mta; + private AutoResetEvent _process = new(false); + private AutoResetEvent _done = new(false); + private ObjectDelegate _doit; + private VoidDelegate _doit2; + private object _result; + private Exception _exception; + + #endregion + } + + internal delegate object ObjectDelegate(); + internal delegate void VoidDelegate(); + } + + #endregion +} diff --git a/src/libraries/System.Speech/src/Internal/SapiInterop/SapiRecoContext.cs b/src/libraries/System.Speech/src/Internal/SapiInterop/SapiRecoContext.cs new file mode 100644 index 00000000000000..19c895c9d3bcdc --- /dev/null +++ b/src/libraries/System.Speech/src/Internal/SapiInterop/SapiRecoContext.cs @@ -0,0 +1,95 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Runtime.InteropServices; + +namespace System.Speech.Internal.SapiInterop +{ + internal class SapiRecoContext : IDisposable + { + #region Constructors + + // This constructor must be called in the context of the background proxy if any + internal SapiRecoContext(ISpRecoContext recoContext, SapiProxy proxy) + { + _recoContext = recoContext; + _proxy = proxy; + } + + public void Dispose() + { + if (!_disposed) + { + // Called from the client proxy + _proxy.Invoke2(delegate { Marshal.ReleaseComObject(_recoContext); }); + _disposed = true; + } + GC.SuppressFinalize(this); + } + + #endregion + + #region Internal Methods + + internal void SetInterest(ulong eventInterest, ulong queuedInterest) + { + _proxy.Invoke2(delegate { _recoContext.SetInterest(eventInterest, queuedInterest); }); + } + + internal SapiGrammar CreateGrammar(ulong id) + { + ISpRecoGrammar sapiGrammar; + return (SapiGrammar)_proxy.Invoke(delegate { _recoContext.CreateGrammar(id, out sapiGrammar); return new SapiGrammar(sapiGrammar, _proxy); }); + } + + internal void SetMaxAlternates(uint count) + { + _proxy.Invoke2(delegate { _recoContext.SetMaxAlternates(count); }); + } + + internal void SetAudioOptions(SPAUDIOOPTIONS options, IntPtr audioFormatId, IntPtr waveFormatEx) + { + _proxy.Invoke2(delegate { _recoContext.SetAudioOptions(options, audioFormatId, waveFormatEx); }); + } + + internal void Bookmark(SPBOOKMARKOPTIONS options, ulong position, IntPtr lparam) + { + _proxy.Invoke2(delegate { _recoContext.Bookmark(options, position, lparam); }); + } + + internal void Resume() + { + _proxy.Invoke2(delegate { _recoContext.Resume(0); }); + } + + internal void SetContextState(SPCONTEXTSTATE state) + { + _proxy.Invoke2(delegate { _recoContext.SetContextState(state); }); + } + + internal EventNotify CreateEventNotify(AsyncSerializedWorker asyncWorker, bool supportsSapi53) + { + return (EventNotify)_proxy.Invoke(delegate { return new EventNotify(_recoContext, asyncWorker, supportsSapi53); }); + } + + internal void DisposeEventNotify(EventNotify eventNotify) + { + _proxy.Invoke2(delegate { eventNotify.Dispose(); }); + } + + internal void SetGrammarOptions(SPGRAMMAROPTIONS options) + { + _proxy.Invoke2(delegate { ((ISpRecoContext2)_recoContext).SetGrammarOptions(options); }); + } + + #endregion + + #region Private Fields + + private ISpRecoContext _recoContext; + private SapiProxy _proxy; + private bool _disposed; + + #endregion + } +} diff --git a/src/libraries/System.Speech/src/Internal/SapiInterop/SapiRecoInterop.cs b/src/libraries/System.Speech/src/Internal/SapiInterop/SapiRecoInterop.cs new file mode 100644 index 00000000000000..2c91bbd961b307 --- /dev/null +++ b/src/libraries/System.Speech/src/Internal/SapiInterop/SapiRecoInterop.cs @@ -0,0 +1,1053 @@ +// 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.Globalization; +using System.Runtime.InteropServices; +using System.Runtime.InteropServices.ComTypes; +using System.Speech.Recognition; + +namespace System.Speech.Internal.SapiInterop +{ + #region Enum + + internal enum SPRECOSTATE + { + SPRST_INACTIVE = 0x00000000, + SPRST_ACTIVE = 0x00000001, + SPRST_ACTIVE_ALWAYS = 0x00000002, + SPRST_INACTIVE_WITH_PURGE = 0x00000003, + SPRST_NUM_STATES = 0x00000004 + } + + internal enum SPLOADOPTIONS + { + SPLO_STATIC = 0x00000000, + SPLO_DYNAMIC = 0x00000001 + } + + internal enum SPRULESTATE + { + SPRS_INACTIVE = 0x00000000, + SPRS_ACTIVE = 0x00000001, + SPRS_ACTIVE_WITH_AUTO_PAUSE = 0x00000003, + SPRS_ACTIVE_USER_DELIMITED = 0x00000004 + } + + internal enum SPGRAMMAROPTIONS + { + SPGO_SAPI = 0x00000001, + SPGO_SRGS = 0x00000002, + SPGO_UPS = 0x00000004, + SPGO_SRGS_MSS_SCRIPT = 0x0008, + SPGO_FILE = 0x00000010, + SPGO_HTTP = 0x00000020, + SPGO_RES = 0x00000040, + SPGO_OBJECT = 0x00000080, + SPGO_SRGS_W3C_SCRIPT = 0x100, + SPGO_SRGS_STG_SCRIPT = 0x200, + + SPGO_SRGS_SCRIPT = SPGO_SRGS | SPGO_SRGS_MSS_SCRIPT | SPGO_SRGS_W3C_SCRIPT | SPGO_SRGS_STG_SCRIPT, + SPGO_DEFAULT = SPGO_SAPI | SPGO_SRGS | SPGO_FILE | SPGO_HTTP | SPGO_RES | SPGO_OBJECT, + SPGO_ALL = SPGO_SAPI | SPGO_SRGS | SPGO_SRGS_SCRIPT | SPGO_FILE | SPGO_HTTP | SPGO_RES | SPGO_OBJECT + } + + internal enum SPSTREAMFORMATTYPE + { + SPWF_INPUT = 0x00000000, + SPWF_SRENGINE = 0x00000001 + } + + [Flags] + internal enum SpeechEmulationCompareFlags + { + SECFIgnoreCase = 0x00000001, + SECFIgnoreKanaType = 0x00010000, + SECFIgnoreWidth = 0x00020000, + SECFNoSpecialChars = 0x20000000, + SECFEmulateResult = 0x40000000, + SECFDefault = SECFIgnoreCase | SECFIgnoreKanaType | SECFIgnoreWidth + } + + [Flags] + internal enum SPADAPTATIONSETTINGS + { + SPADS_Default = 0x0000, + SPADS_CurrentRecognizer = 0x0001, + SPADS_RecoProfile = 0x0002, + SPADS_Immediate = 0x0004, + SPADS_Reset = 0x0008 + } + + internal enum SPADAPTATIONRELEVANCE + { + SPAR_Unknown = 0, + SPAR_Low = 1, + SPAR_Medium = 2, + SPAR_High = 3 + } + + [Flags] + internal enum SPRECOEVENTFLAGS + { + SPREF_AutoPause = 0x0001, + SPREF_Emulated = 0x0002, + SPREF_SMLTimeout = 0x0004, + SPREF_ExtendableParse = 0x0008, + SPREF_ReSent = 0x0010, + SPREF_Hypothesis = 0x0020, + SPREF_FalseRecognition = 0x0040 + } + + [Flags] + internal enum SPBOOKMARKOPTIONS + { + SPBO_NONE = 0x0000, + SPBO_PAUSE = 0x0001, + SPBO_AHEAD = 0x0002, + SPBO_TIME_UNITS = 0x0004 + } + + internal enum SPAUDIOOPTIONS + { + SPAO_NONE = 0, + SPAO_RETAIN_AUDIO = 1 + } + + [Flags] + internal enum SPENDSRSTREAMFLAGS + { + SPESF_NONE = 0x00, + SPESF_STREAM_RELEASED = 0x01, + SPESF_EMULATED = 0x02 + }; + + [Flags] + internal enum SPCOMMITFLAGS + { + SPCF_NONE = 0x00, + SPCF_ADD_TO_USER_LEXICON = 0x01, + SPCF_DEFINITE_CORRECTION = 0x02 + }; + + internal enum SPAUDIOSTATE + { + SPAS_CLOSED = 0, + SPAS_STOP = 1, + SPAS_PAUSE = 2, + SPAS_RUN = 3 + } + + internal enum SPXMLRESULTOPTIONS + { + SPXRO_SML = 0x00000000, + SPXRO_Alternates_SML = 0x00000001 + } + + internal enum SPCONTEXTSTATE + { + SPCS_DISABLED = 0, + SPCS_ENABLED = 1 + } + + internal enum SPINTERFERENCE + { + SPINTERFERENCE_NONE = 0, + SPINTERFERENCE_NOISE = 1, + SPINTERFERENCE_NOSIGNAL = 2, + SPINTERFERENCE_TOOLOUD = 3, + SPINTERFERENCE_TOOQUIET = 4, + SPINTERFERENCE_TOOFAST = 5, + SPINTERFERENCE_TOOSLOW = 6 + } + + internal enum SPGRAMMARSTATE + { + SPGS_DISABLED = 0, + SPGS_ENABLED = 1, + SPGS_EXCLUSIVE = 3 + } + + [Flags] + internal enum SPRESULTALPHABET + { + SPRA_NONE = 0, + SPRA_APP_UPS = 0x0001, + SPRA_ENGINE_UPS = 0x0002 + } + + #endregion + + #region Structure + + /// Note: This structure doesn't exist in SAPI.idl but is related to SPPHRASEALT. + /// We use it to map memory contained in the serialized result (instead of reading sequentially) + [StructLayout(LayoutKind.Sequential)] + internal class SPSERIALIZEDPHRASEALT + { + internal uint ulStartElementInParent; + internal uint cElementsInParent; + internal uint cElementsInAlternate; + internal uint cbAltExtra; + } + + [StructLayout(LayoutKind.Sequential)] + [Serializable] + internal struct FILETIME + { + internal uint dwLowDateTime; + internal uint dwHighDateTime; + } + + [StructLayout(LayoutKind.Sequential)] + [Serializable] + internal struct SPRECORESULTTIMES + { + internal FILETIME ftStreamTime; + internal ulong ullLength; + internal uint dwTickCount; + internal ulong ullStart; + } + + internal struct SPTEXTSELECTIONINFO + { + internal uint ulStartActiveOffset; + internal uint cchActiveChars; + internal uint ulStartSelection; + internal uint cchSelection; + + internal SPTEXTSELECTIONINFO(uint ulStartActiveOffset, uint cchActiveChars, + uint ulStartSelection, uint cchSelection) + { + this.ulStartActiveOffset = ulStartActiveOffset; + this.cchActiveChars = cchActiveChars; + this.ulStartSelection = ulStartSelection; + this.cchSelection = cchSelection; + } + } + + [StructLayout(LayoutKind.Sequential)] + internal struct SPAUDIOSTATUS + { + internal int cbFreeBuffSpace; + internal uint cbNonBlockingIO; + internal SPAUDIOSTATE State; + internal ulong CurSeekPos; + internal ulong CurDevicePos; + internal uint dwAudioLevel; + internal uint dwReserved2; + } + + [StructLayout(LayoutKind.Sequential)] + internal struct SPRECOGNIZERSTATUS + { + internal SPAUDIOSTATUS AudioStatus; + internal ulong ullRecognitionStreamPos; + internal uint ulStreamNumber; + internal uint ulNumActive; + internal Guid clsidEngine; + internal uint cLangIDs; + [MarshalAs(UnmanagedType.ByValArray, SizeConst = 20)] // SP_MAX_LANGIDS + internal short[] aLangID; + internal ulong ullRecognitionStreamTime; + } + + [StructLayout(LayoutKind.Sequential)] + internal struct SPRECOCONTEXTSTATUS + { + internal SPINTERFERENCE eInterference; + [MarshalAs(UnmanagedType.ByValArray, SizeConst = 255)] + internal short[] szRequestTypeOfUI; // Can't really be marshaled as a string directly + internal uint dwReserved1; + internal uint dwReserved2; + } + + [StructLayout(LayoutKind.Sequential)] + internal class SPSEMANTICERRORINFO + { + internal uint ulLineNumber; + internal uint pszScriptLineOffset; + internal uint pszSourceOffset; + internal uint pszDescriptionOffset; + internal int hrResultCode; + } + + [StructLayout(LayoutKind.Sequential)] + internal struct SPSERIALIZEDRESULT + { + internal uint ulSerializedSize; // Count in bytes (including this ULONG) of the entire phrase + } + + // Serialized result header from versions of SAPI prior to 5.3. + [StructLayout(LayoutKind.Sequential)] + [Serializable] + internal class SPRESULTHEADER_Sapi51 + { + internal uint ulSerializedSize; // This MUST be the first field to line up with SPSERIALIZEDRESULT + internal uint cbHeaderSize; // This must be sizeof(SPRESULTHEADER), or sizeof(SPRESULTHEADER_Sapi51) on SAPI 5.1. + internal Guid clsidEngine; // CLSID clsidEngine; + internal Guid clsidAlternates; // CLSID clsidAlternates; + internal uint ulStreamNum; + internal ulong ullStreamPosStart; + internal ulong ullStreamPosEnd; + internal uint ulPhraseDataSize; // byte size of all the phrase structure + internal uint ulPhraseOffset; // offset to phrase + internal uint ulPhraseAltDataSize; // byte size of all the phrase alt structures combined + internal uint ulPhraseAltOffset; // offset to phrase + internal uint ulNumPhraseAlts; // Number of alts in array + internal uint ulRetainedDataSize; // byte size of audio data + internal uint ulRetainedOffset; // offset to audio data in this phrase blob + internal uint ulDriverDataSize; // byte size of driver specific data + internal uint ulDriverDataOffset; // offset to driver specific data + internal float fTimePerByte; // Conversion factor from engine stream size to time. + internal float fInputScaleFactor; // Conversion factor from engine stream size to input stream size. + internal SPRECORESULTTIMES times; // time info of result + } + + // The SAPI 5.3 result header added extra fields. + [StructLayout(LayoutKind.Sequential)] + [Serializable] + internal class SPRESULTHEADER + { + internal SPRESULTHEADER() + { + } + + internal SPRESULTHEADER(SPRESULTHEADER_Sapi51 source) + { + ulSerializedSize = source.ulSerializedSize; + cbHeaderSize = source.cbHeaderSize; + clsidEngine = source.clsidEngine; + clsidAlternates = source.clsidAlternates; + ulStreamNum = source.ulStreamNum; + ullStreamPosStart = source.ullStreamPosStart; + ullStreamPosEnd = source.ullStreamPosEnd; + ulPhraseDataSize = source.ulPhraseDataSize; + ulPhraseOffset = source.ulPhraseOffset; + ulPhraseAltDataSize = source.ulPhraseAltDataSize; + ulPhraseAltOffset = source.ulPhraseAltOffset; + ulNumPhraseAlts = source.ulNumPhraseAlts; + ulRetainedDataSize = source.ulRetainedDataSize; + ulRetainedOffset = source.ulRetainedOffset; + ulDriverDataSize = source.ulDriverDataSize; + ulDriverDataOffset = source.ulDriverDataOffset; + fTimePerByte = source.fTimePerByte; + fInputScaleFactor = source.fInputScaleFactor; + times = source.times; + } + + internal void Validate() + { + ValidateOffsetAndLength(0, cbHeaderSize); + ValidateOffsetAndLength(ulPhraseOffset, ulPhraseDataSize); + ValidateOffsetAndLength(ulPhraseAltOffset, ulPhraseAltDataSize); + ValidateOffsetAndLength(ulRetainedOffset, ulRetainedDataSize); + ValidateOffsetAndLength(ulDriverDataOffset, ulDriverDataSize); + } + + // Duplicate all the fields of SPRESULTHEADER_Sapi51 - Marshal.PtrToStructure seems to need these to be defined again. + internal uint ulSerializedSize; + internal uint cbHeaderSize; + internal Guid clsidEngine; + internal Guid clsidAlternates; + internal uint ulStreamNum; + internal ulong ullStreamPosStart; + internal ulong ullStreamPosEnd; + internal uint ulPhraseDataSize; + internal uint ulPhraseOffset; + internal uint ulPhraseAltDataSize; + internal uint ulPhraseAltOffset; + internal uint ulNumPhraseAlts; + internal uint ulRetainedDataSize; + internal uint ulRetainedOffset; + internal uint ulDriverDataSize; + internal uint ulDriverDataOffset; + internal float fTimePerByte; + internal float fInputScaleFactor; + internal SPRECORESULTTIMES times; + + private void ValidateOffsetAndLength(uint offset, uint length) + { + if (offset + length > ulSerializedSize) + { + throw new FormatException(SR.Get(SRID.ResultInvalidFormat)); + } + } + internal uint fAlphabet; + // Not present in SAPI 5.1 results; on SAPI 5.without IPA this is set to zero, with IPA it will indicate + // the alphabet of pronunciations the result + } + + // Serialized phrase header from versions of SAPI prior to 5.2. + [StructLayout(LayoutKind.Sequential)] + internal class SPSERIALIZEDPHRASE_Sapi51 + { + internal uint ulSerializedSize; // This MUST be the first field to line up with SPSERIALIZEDPHRASE + internal uint cbSize; // size of just this structure within the serialized block header") + internal ushort LangID; + internal ushort wHomophoneGroupId; + internal ulong ullGrammarID; + internal ulong ftStartTime; + internal ulong ullAudioStreamPosition; + internal uint ulAudioSizeBytes; + internal uint ulRetainedSizeBytes; + internal uint ulAudioSizeTime; + internal SPSERIALIZEDPHRASERULE Rule; + internal uint PropertiesOffset; + internal uint ElementsOffset; + internal uint cReplacements; + internal uint ReplacementsOffset; + internal Guid SREngineID; + internal uint ulSREnginePrivateDataSize; + internal uint SREnginePrivateDataOffset; + } + + [StructLayout(LayoutKind.Sequential)] + [Serializable] + internal class SPPHRASE + { + internal uint cbSize; // Size of structure + internal ushort LangID; + internal ushort wReserved; + internal ulong ullGrammarID; + internal ulong ftStartTime; + internal ulong ullAudioStreamPosition; + internal uint ulAudioSizeBytes; + internal uint ulRetainedSizeBytes; + internal uint ulAudioSizeTime; // In 100ns units + internal SPPHRASERULE Rule; + internal IntPtr pProperties; + internal IntPtr pElements; + internal uint cReplacements; + internal IntPtr pReplacements; + internal Guid SREngineID; + internal uint ulSREnginePrivateDataSize; + internal IntPtr pSREnginePrivateData; + + /// + /// Helper function used to create a new phrase object from a + /// test string. Each word in the string is converted to a phrase element. + /// This is useful to create a phrase to pass to the EmulateRecognition method. + /// + internal static ISpPhrase CreatePhraseFromText(string phrase, CultureInfo culture, out GCHandle[] memHandles, out IntPtr coMem) + { + string[] words = phrase.Split(Array.Empty(), StringSplitOptions.RemoveEmptyEntries); + RecognizedWordUnit[] wordUnits = new RecognizedWordUnit[words.Length]; + for (int i = 0; i < wordUnits.Length; i++) + { + wordUnits[i] = new RecognizedWordUnit(null, 1.0f, null, words[i], DisplayAttributes.OneTrailingSpace, TimeSpan.Zero, TimeSpan.Zero); + } + return CreatePhraseFromWordUnits(wordUnits, culture, out memHandles, out coMem); + } + + /// + /// Helper function used to create a new phrase object from a + /// test string. Each word in the string is converted to a phrase element. + /// This is useful to create a phrase to pass to the EmulateRecognition method. + /// + internal static ISpPhrase CreatePhraseFromWordUnits(RecognizedWordUnit[] words, CultureInfo culture, out GCHandle[] memHandles, out IntPtr coMem) + { + SPPHRASEELEMENT[] elements = new SPPHRASEELEMENT[words.Length]; + + // build the unmanaged interop layer + int size = Marshal.SizeOf(typeof(SPPHRASEELEMENT)); + List handles = new(); + + coMem = Marshal.AllocCoTaskMem(size * elements.Length); + try + { + for (int i = 0; i < words.Length; i++) + { + RecognizedWordUnit word = words[i]; + elements[i] = new SPPHRASEELEMENT + { + // display + confidence + bDisplayAttributes = RecognizedWordUnit.DisplayAttributesToSapiAttributes(word.DisplayAttributes == DisplayAttributes.None ? DisplayAttributes.OneTrailingSpace : word.DisplayAttributes), + SREngineConfidence = word.Confidence, + + // Timing information + ulAudioTimeOffset = unchecked((uint)(word._audioPosition.Ticks * 10000 / TimeSpan.TicksPerMillisecond)), + ulAudioSizeTime = unchecked((uint)(word._audioDuration.Ticks * 10000 / TimeSpan.TicksPerMillisecond)) + }; + + // DLP information + if (word.Text != null) + { + GCHandle handle = GCHandle.Alloc(word.Text, GCHandleType.Pinned); + handles.Add(handle); + elements[i].pszDisplayText = handle.AddrOfPinnedObject(); + } + + if (word.Text == null || word.LexicalForm != word.Text) + { + GCHandle handle = GCHandle.Alloc(word.LexicalForm, GCHandleType.Pinned); + handles.Add(handle); + elements[i].pszLexicalForm = handle.AddrOfPinnedObject(); + } + else + { + elements[i].pszLexicalForm = elements[i].pszDisplayText; + } + + if (!string.IsNullOrEmpty(word.Pronunciation)) + { + GCHandle handle = GCHandle.Alloc(word.Pronunciation, GCHandleType.Pinned); + handles.Add(handle); + elements[i].pszPronunciation = handle.AddrOfPinnedObject(); + } + + Marshal.StructureToPtr(elements[i], new IntPtr((long)coMem + size * i), false); + } + } + finally + { + memHandles = handles.ToArray(); + } + + SPPHRASE spPhrase = new(); + spPhrase.cbSize = (uint)Marshal.SizeOf(spPhrase.GetType()); + spPhrase.LangID = (ushort)culture.LCID; + spPhrase.Rule = new SPPHRASERULE + { + ulCountOfElements = (uint)words.Length + }; + + spPhrase.pElements = coMem; + + // Initialized the phrase + SpPhraseBuilder phraseBuilder = new(); + ((ISpPhraseBuilder)phraseBuilder).InitFromPhrase(spPhrase); + + return (ISpPhrase)phraseBuilder; + } + } + + [StructLayout(LayoutKind.Sequential)] + [Serializable] + internal class SPPHRASERULE + { + [MarshalAs(UnmanagedType.LPWStr)] + internal string pszName; + internal uint ulId; + internal uint ulFirstElement; + internal uint ulCountOfElements; + internal IntPtr pNextSibling; + internal IntPtr pFirstChild; + internal float SREngineConfidence; + internal byte Confidence; + } + + [StructLayout(LayoutKind.Sequential)] + [Serializable] + internal class SPPHRASEELEMENT + { + internal uint ulAudioTimeOffset; + internal uint ulAudioSizeTime; // In 100ns units + internal uint ulAudioStreamOffset; + internal uint ulAudioSizeBytes; + internal uint ulRetainedStreamOffset; + internal uint ulRetainedSizeBytes; + internal IntPtr pszDisplayText; + internal IntPtr pszLexicalForm; + internal IntPtr pszPronunciation; + internal byte bDisplayAttributes; + internal byte RequiredConfidence; + internal byte ActualConfidence; + internal byte Reserved; + internal float SREngineConfidence; + } + + // The SAPI 5.2 & 5.3 result header added extra fields. + [StructLayout(LayoutKind.Sequential)] + [Serializable] + internal class SPSERIALIZEDPHRASE + { + internal SPSERIALIZEDPHRASE() + { } + + internal SPSERIALIZEDPHRASE(SPSERIALIZEDPHRASE_Sapi51 source) + { + ulSerializedSize = source.ulSerializedSize; + cbSize = source.cbSize; + LangID = source.LangID; + wHomophoneGroupId = source.wHomophoneGroupId; + ullGrammarID = source.ullGrammarID; + ftStartTime = source.ftStartTime; + ullAudioStreamPosition = source.ullAudioStreamPosition; + ulAudioSizeBytes = source.ulAudioSizeBytes; + ulRetainedSizeBytes = source.ulRetainedSizeBytes; + ulAudioSizeTime = source.ulAudioSizeTime; + Rule = source.Rule; + PropertiesOffset = source.PropertiesOffset; + ElementsOffset = source.ElementsOffset; + cReplacements = source.cReplacements; + ReplacementsOffset = source.ReplacementsOffset; + SREngineID = source.SREngineID; + ulSREnginePrivateDataSize = source.ulSREnginePrivateDataSize; + SREnginePrivateDataOffset = source.SREnginePrivateDataOffset; + } + + // Duplicate all the fields of SPSERIALIZEDPHRASE_Sapi51 - Marshal.PtrToStructure seems to need these to be defined again. + internal uint ulSerializedSize; + internal uint cbSize; + internal ushort LangID; + internal ushort wHomophoneGroupId; + internal ulong ullGrammarID; + internal ulong ftStartTime; + internal ulong ullAudioStreamPosition; + internal uint ulAudioSizeBytes; + internal uint ulRetainedSizeBytes; + internal uint ulAudioSizeTime; + internal SPSERIALIZEDPHRASERULE Rule; + internal uint PropertiesOffset; + internal uint ElementsOffset; + internal uint cReplacements; + internal uint ReplacementsOffset; + internal Guid SREngineID; + internal uint ulSREnginePrivateDataSize; + internal uint SREnginePrivateDataOffset; + + internal uint SMLOffset; // Not present in SAPI 5.1 results. + internal uint SemanticErrorInfoOffset; // Not present in SAPI 5.1 results. + } + + [StructLayout(LayoutKind.Sequential)] + [Serializable] + internal class SPSERIALIZEDPHRASERULE + { + internal uint pszNameOffset; + internal uint ulId; + internal uint ulFirstElement; + internal uint ulCountOfElements; + internal uint NextSiblingOffset; + internal uint FirstChildOffset; + internal float SREngineConfidence; + internal sbyte Confidence; + } + + [StructLayout(LayoutKind.Sequential)] + internal class SPSERIALIZEDPHRASEELEMENT + { + internal uint ulAudioTimeOffset; + internal uint ulAudioSizeTime; // In 100ns units + internal uint ulAudioStreamOffset; + internal uint ulAudioSizeBytes; + internal uint ulRetainedStreamOffset; + internal uint ulRetainedSizeBytes; + internal uint pszDisplayTextOffset; + internal uint pszLexicalFormOffset; + internal uint pszPronunciationOffset; + internal byte bDisplayAttributes; + internal char RequiredConfidence; + internal char ActualConfidence; + internal byte Reserved; + internal float SREngineConfidence; + } + + [StructLayout(LayoutKind.Sequential)] + internal class SPSERIALIZEDPHRASEPROPERTY + { + internal uint pszNameOffset; + internal uint ulId; + internal uint pszValueOffset; + internal ushort vValue; // sizeof unsigned short + internal ulong SpVariantSubset; // sizeof DOUBLE + internal uint ulFirstElement; + internal uint ulCountOfElements; + internal uint pNextSiblingOffset; + internal uint pFirstChildOffset; + internal float SREngineConfidence; + internal sbyte Confidence; + } + + [StructLayout(LayoutKind.Sequential)] + internal class SPPHRASEREPLACEMENT + { + internal byte bDisplayAttributes; + internal uint pszReplacementText; + internal uint ulFirstElement; + internal uint ulCountOfElements; + } + + [StructLayout(LayoutKind.Sequential)] + internal class SPWAVEFORMATEX + { + public uint cbUsed; + public Guid Guid; + public ushort wFormatTag; + public ushort nChannels; + public uint nSamplesPerSec; + public uint nAvgBytesPerSec; + public ushort nBlockAlign; + public ushort wBitsPerSample; + public ushort cbSize; + } + + #endregion + + #region Interface + + [ComImport, Guid("8137828F-591A-4A42-BE58-49EA7EBAAC68"), InterfaceType(ComInterfaceType.InterfaceIsIUnknown)] + internal interface ISpGrammarBuilder + { + // ISpGrammarBuilder Methods + void Slot1(); // void ResetGrammar(UInt16 NewLanguage); + void Slot2(); // void GetRule([MarshalAs(UnmanagedType.LPWStr)] string pszRuleName, UInt32 dwRuleId, UInt32 dwAttributes, [MarshalAs(UnmanagedType.Bool)] bool fCreateIfNotExist, out IntPtr phInitialState); + void Slot3(); // void ClearRule(IntPtr hState); + void Slot4(); // void CreateNewState(IntPtr hState, out IntPtr phState); + void Slot5(); // void AddWordTransition(IntPtr hFromState, IntPtr hToState, [MarshalAs(UnmanagedType.LPWStr)] string psz, [MarshalAs(UnmanagedType.LPWStr)] string pszSeparators, SPGRAMMARWORDTYPE eWordType, float Weight, ref SPPROPERTYINFO pPropInfo); + void Slot6(); // void AddRuleTransition(IntPtr hFromState, IntPtr hToState, IntPtr hRule, float Weight, ref SPPROPERTYINFO pPropInfo); + void Slot7(); // void AddResource(IntPtr hRuleState, [MarshalAs(UnmanagedType.LPWStr)] string pszResourceName, [MarshalAs(UnmanagedType.LPWStr)] string pszResourceValue); + void Slot8(); // void Commit(UInt32 dwReserved); + } + + [ComImport, Guid("2177DB29-7F45-47D0-8554-067E91C80502"), InterfaceType(ComInterfaceType.InterfaceIsIUnknown)] + internal interface ISpRecoGrammar : ISpGrammarBuilder + { + // ISpGrammarBuilder Methods + new void Slot1(); // void ResetGrammar(UInt16 NewLanguage); + new void Slot2(); // void GetRule([MarshalAs(UnmanagedType.LPWStr)] string pszRuleName, UInt32 dwRuleId, UInt32 dwAttributes, [MarshalAs(UnmanagedType.Bool)] bool fCreateIfNotExist, out IntPtr phInitialState); + new void Slot3(); // void ClearRule(IntPtr hState); + new void Slot4(); // void CreateNewState(IntPtr hState, out IntPtr phState); + new void Slot5(); // void AddWordTransition(IntPtr hFromState, IntPtr hToState, [MarshalAs(UnmanagedType.LPWStr)] string psz, [MarshalAs(UnmanagedType.LPWStr)] string pszSeparators, SPGRAMMARWORDTYPE eWordType, float Weight, ref SPPROPERTYINFO pPropInfo); + new void Slot6(); // void AddRuleTransition(IntPtr hFromState, IntPtr hToState, IntPtr hRule, float Weight, ref SPPROPERTYINFO pPropInfo); + new void Slot7(); // void AddResource(IntPtr hRuleState, [MarshalAs(UnmanagedType.LPWStr)] string pszResourceName, [MarshalAs(UnmanagedType.LPWStr)] string pszResourceValue); + new void Slot8(); // void Commit(UInt32 dwReserved); + + // ISpRecoGrammar Methods + void Slot9(); // void GetGrammarId(out UInt64 pullGrammarId); + void Slot10(); // void GetRecoContext(out ISpRecoContext ppRecoCtxt); + void LoadCmdFromFile([MarshalAs(UnmanagedType.LPWStr)] string pszFileName, SPLOADOPTIONS Options); + void Slot12(); // void LoadCmdFromObject(ref Guid rcid, string pszGrammarName, SPLOADOPTIONS Options); + void Slot13(); // void LoadCmdFromResource(IntPtr hModule, string pszResourceName, string pszResourceType, UInt16 wLanguage, SPLOADOPTIONS Options); + void LoadCmdFromMemory(IntPtr pGrammar, SPLOADOPTIONS Options); + void Slot15(); // void LoadCmdFromProprietaryGrammar(ref Guid rguidParam, string pszStringParam, IntPtr pvDataPrarm, UInt32 cbDataSize, SPLOADOPTIONS Options); + [PreserveSig] + int SetRuleState([MarshalAs(UnmanagedType.LPWStr)] string pszName, IntPtr pReserved, SPRULESTATE NewState); + void Slot17(); // void SetRuleIdState(UInt32 ulRuleId, SPRULESTATE NewState); + void LoadDictation([MarshalAs(UnmanagedType.LPWStr)] string pszTopicName, SPLOADOPTIONS Options); + void Slot19(); // void UnloadDictation(); + [PreserveSig] + int SetDictationState(SPRULESTATE NewState); + void SetWordSequenceData([MarshalAs(UnmanagedType.LPWStr)] string pText, uint cchText, ref SPTEXTSELECTIONINFO pInfo); + void SetTextSelection(ref SPTEXTSELECTIONINFO pInfo); + void Slot23(); // void IsPronounceable(string pszWord, out SPWORDPRONOUNCEABLE pWordPronounceable); + void SetGrammarState(SPGRAMMARSTATE eGrammarState); + void Slot25(); // void SaveCmd(IStream pStream, IntPtr ppszCoMemErrorText); + void Slot26(); // void GetGrammarState(out SPGRAMMARSTATE peGrammarState); + } + + [ComImport, Guid("4B37BC9E-9ED6-44a3-93D3-18F022B79EC3"), InterfaceType(ComInterfaceType.InterfaceIsIUnknown)] + internal interface ISpRecoGrammar2 + { + void GetRules(out IntPtr ppCoMemRules, out uint puNumRules); + void LoadCmdFromFile2([MarshalAs(UnmanagedType.LPWStr)] string pszFileName, SPLOADOPTIONS Options, [MarshalAs(UnmanagedType.LPWStr)] string pszSharingUri, [MarshalAs(UnmanagedType.LPWStr)] string pszBaseUri); + void LoadCmdFromMemory2(IntPtr pGrammar, SPLOADOPTIONS Options, [MarshalAs(UnmanagedType.LPWStr)] string pszSharingUri, [MarshalAs(UnmanagedType.LPWStr)] string pszBaseUri); + void SetRulePriority([MarshalAs(UnmanagedType.LPWStr)] string pszRuleName, uint ulRuleId, int nRulePriority); + void SetRuleWeight([MarshalAs(UnmanagedType.LPWStr)] string pszRuleName, uint ulRuleId, float flWeight); + void SetDictationWeight(float flWeight); + void SetGrammarLoader(ISpGrammarResourceLoader pLoader); + void Slot2(); //HRESULT SetSMLSecurityManager([in] IInternetSecurityManager* pSMLSecurityManager); + } + + [ComImport, Guid("F740A62F-7C15-489E-8234-940A33D9272D"), InterfaceType(ComInterfaceType.InterfaceIsIUnknown)] + internal interface ISpRecoContext : ISpEventSource + { + // ISpNotifySource Methods + new void SetNotifySink(ISpNotifySink pNotifySink); + new void SetNotifyWindowMessage(uint hWnd, uint Msg, IntPtr wParam, IntPtr lParam); + new void Slot3(); // void SetNotifyCallbackFunction(ref IntPtr pfnCallback, IntPtr wParam, IntPtr lParam); + new void Slot4(); // void SetNotifyCallbackInterface(ref IntPtr pSpCallback, IntPtr wParam, IntPtr lParam); + new void Slot5(); // void SetNotifyWin32Event(); + [PreserveSig] + new int WaitForNotifyEvent(uint dwMilliseconds); + new void Slot7(); // IntPtr GetNotifyEventHandle(); + + // ISpEventSource Methods + new void SetInterest(ulong ullEventInterest, ulong ullQueuedInterest); + new void GetEvents(uint ulCount, out SPEVENT pEventArray, out uint pulFetched); + new void Slot10(); // void GetInfo(out SPEVENTSOURCEINFO pInfo); + + // ISpRecoContext Methods + void GetRecognizer(out ISpRecognizer ppRecognizer); + void CreateGrammar(ulong ullGrammarID, out ISpRecoGrammar ppGrammar); + void GetStatus(out SPRECOCONTEXTSTATUS pStatus); + void GetMaxAlternates(out uint pcAlternates); + void SetMaxAlternates(uint cAlternates); + void SetAudioOptions(SPAUDIOOPTIONS Options, IntPtr pAudioFormatId, IntPtr pWaveFormatEx); + void Slot17(); // void GetAudioOptions(out SPAUDIOOPTIONS pOptions, out Guid pAudioFormatId, out IntPtr ppCoMemWFEX); + void Slot18(); // void DeserializeResult(ref SPSERIALIZEDRESULT pSerializedResult, out ISpRecoResult ppResult); + void Bookmark(SPBOOKMARKOPTIONS Options, ulong ullStreamPosition, IntPtr lparamEvent); + void Slot20(); // void SetAdaptationData([MarshalAs(UnmanagedType.LPWStr)] string pAdaptationData, UInt32 cch); + void Pause(uint dwReserved); + void Resume(uint dwReserved); + void Slot23(); // void SetVoice (ISpVoice pVoice, [MarshalAs (UnmanagedType.Bool)] bool fAllowFormatChanges); + void Slot24(); // void GetVoice(out ISpVoice ppVoice); + void Slot25(); // void SetVoicePurgeEvent(UInt64 ullEventInterest); + void Slot26(); // void GetVoicePurgeEvent(out UInt64 pullEventInterest); + void SetContextState(SPCONTEXTSTATE eContextState); + void Slot28(); // void GetContextState(out SPCONTEXTSTATE peContextState); + } + + [ComImport, Guid("BEAD311C-52FF-437f-9464-6B21054CA73D"), InterfaceType(ComInterfaceType.InterfaceIsIUnknown)] + internal interface ISpRecoContext2 + { + // ISpRecoContext2 Methods + void SetGrammarOptions(SPGRAMMAROPTIONS eGrammarOptions); + void Slot2(); // void GetGrammarOptions(out SPGRAMMAROPTIONS peGrammarOptions); + void SetAdaptationData2([MarshalAs(UnmanagedType.LPWStr)] string pAdaptationData, uint cch, [MarshalAs(UnmanagedType.LPWStr)] string pTopicName, SPADAPTATIONSETTINGS eSettings, SPADAPTATIONRELEVANCE eRelevance); + } + + [ComImport, Guid("5B4FB971-B115-4DE1-AD97-E482E3BF6EE4"), InterfaceType(ComInterfaceType.InterfaceIsIUnknown)] + internal interface ISpProperties + { + // ISpProperties Methods + [PreserveSig] + int SetPropertyNum([MarshalAs(UnmanagedType.LPWStr)] string pName, int lValue); + [PreserveSig] + int GetPropertyNum([MarshalAs(UnmanagedType.LPWStr)] string pName, out int plValue); + [PreserveSig] + int SetPropertyString([MarshalAs(UnmanagedType.LPWStr)] string pName, [MarshalAs(UnmanagedType.LPWStr)] string pValue); + [PreserveSig] + int GetPropertyString([MarshalAs(UnmanagedType.LPWStr)] string pName, [MarshalAs(UnmanagedType.LPWStr)] out string ppCoMemValue); + } + + [ComImport, Guid("C2B5F241-DAA0-4507-9E16-5A1EAA2B7A5C"), InterfaceType(ComInterfaceType.InterfaceIsIUnknown)] + internal interface ISpRecognizer : ISpProperties + { + // ISpProperties Methods + [PreserveSig] + new int SetPropertyNum([MarshalAs(UnmanagedType.LPWStr)] string pName, int lValue); + [PreserveSig] + new int GetPropertyNum([MarshalAs(UnmanagedType.LPWStr)] string pName, out int plValue); + [PreserveSig] + new int SetPropertyString([MarshalAs(UnmanagedType.LPWStr)] string pName, [MarshalAs(UnmanagedType.LPWStr)] string pValue); + [PreserveSig] + new int GetPropertyString([MarshalAs(UnmanagedType.LPWStr)] string pName, [MarshalAs(UnmanagedType.LPWStr)] out string ppCoMemValue); + + // ISpRecognizer Methods + void SetRecognizer(ISpObjectToken pRecognizer); + void GetRecognizer(out ISpObjectToken ppRecognizer); + void SetInput([MarshalAs(UnmanagedType.IUnknown)] object pUnkInput, [MarshalAs(UnmanagedType.Bool)] bool fAllowFormatChanges); + void Slot8(); // void GetInputObjectToken(out ISpObjectToken ppToken); + void Slot9(); // void GetInputStream(out ISpStreamFormat ppStream); + void CreateRecoContext(out ISpRecoContext ppNewCtxt); + void Slot11();//void GetRecoProfile(out ISpObjectToken ppToken); + void Slot12(); // void SetRecoProfile(ISpObjectToken pToken); + void Slot13(); // void IsSharedInstance(); + void GetRecoState(out SPRECOSTATE pState); + void SetRecoState(SPRECOSTATE NewState); + void GetStatus(out SPRECOGNIZERSTATUS pStatus); + void GetFormat(SPSTREAMFORMATTYPE WaveFormatType, out Guid pFormatId, out IntPtr ppCoMemWFEX); + void IsUISupported([MarshalAs(UnmanagedType.LPWStr)] string pszTypeOfUI, IntPtr pvExtraData, uint cbExtraData, [MarshalAs(UnmanagedType.Bool)] out bool pfSupported); + [PreserveSig] + int DisplayUI(IntPtr hWndParent, [MarshalAs(UnmanagedType.LPWStr)] string pszTitle, [MarshalAs(UnmanagedType.LPWStr)] string pszTypeOfUI, IntPtr pvExtraData, uint cbExtraData); + [PreserveSig] + int EmulateRecognition(ISpPhrase pPhrase); + } + + [ComImport, Guid("8FC6D974-C81E-4098-93C5-0147F61ED4D3"), InterfaceType(ComInterfaceType.InterfaceIsIUnknown)] + internal interface ISpRecognizer2 + { + // ISpRecognizer2 Methods + [PreserveSig] + int EmulateRecognitionEx(ISpPhrase pPhrase, uint dwCompareFlags); + void SetTrainingState(bool fDoingTraining, bool fAdaptFromTrainingData); + void ResetAcousticModelAdaptation(); + } + + [ComImport, Guid("2D5F1C0C-BD75-4b08-9478-3B11FEA2586C")] + internal interface ISpeechRecognizer + { + // ISpeechRecognizer Methods + object Slot1 { get; set; } // [DispId(1)] SpObjectToken Recognizer { set; get; } + object Slot2 { get; set; } // [DispId(2)] bool AllowAudioInputFormatChangesOnNextSet { set; get; } + object Slot3 { get; set; } // [DispId(3)] SpObjectToken AudioInput { set; get; } + object Slot4 { get; set; } // [DispId(4)] ISpeechBaseStream AudioInputStream { set; get; } + object Slot5 { get; } // [DispId(5)] bool IsShared { get; } + object Slot6 { get; set; } // [DispId(8)] SpObjectToken Profile { set; get; } + object Slot7 { get; set; } // [DispId(6)] SpeechRecognizerState State { set; get; } + object Slot8 { get; } // [DispId(7)] ISpeechRecognizerStatus Status { get; } + [DispId(9)] + [PreserveSig] + int EmulateRecognition(object TextElements, ref object ElementDisplayAttributes, int LanguageId); + void Slot10(); // [DispId(10)] ISpeechRecoContext CreateRecoContext(); + void Slot11(); // [DispId(11)] SpAudioFormat GetFormat(SpeechFormatType Type); + void Slot12(); // [DispId(12)] bool SetPropertyNumber(string Name, Int32 Value); + void Slot13(); // [DispId(13)] bool GetPropertyNumber(string Name, out Int32 Value); + void Slot14(); // [DispId(14)] bool SetPropertyString(string Name, string Value); + void Slot15(); // [DispId(15)] bool GetPropertyString(string Name, out string Value); + void Slot16(); // [DispId(16)] bool IsUISupported(string TypeOfUI, ref Object ExtraData); + void Slot17(); // [DispId(17)] void DisplayUI(Int32 hWndParent, string Title, string TypeOfUI, ref Object ExtraData); + void Slot18(); // [DispId(18)] ISpeechObjectTokens GetRecognizers(string RequiredAttributes, string OptionalAttributes); + void Slot19(); // [DispId(19)] ISpeechObjectTokens GetAudioInputs(string RequiredAttributes, string OptionalAttributes); + void Slot20(); // [DispId(20)] ISpeechObjectTokens GetProfiles(string RequiredAttributes, string OptionalAttributes); + } + + [ComImport, Guid("1A5C0354-B621-4b5a-8791-D306ED379E53"), InterfaceType(ComInterfaceType.InterfaceIsIUnknown)] + internal interface ISpPhrase + { + // ISpPhrase Methods + void GetPhrase(out IntPtr ppCoMemPhrase); + void GetSerializedPhrase(out IntPtr ppCoMemPhrase); + void GetText(uint ulStart, uint ulCount, [MarshalAs(UnmanagedType.Bool)] bool fUseTextReplacements, [MarshalAs(UnmanagedType.LPWStr)] out string ppszCoMemText, out byte pbDisplayAttributes); + void Discard(uint dwValueTypes); + } + + [ComImport, Guid("20B053BE-E235-43cd-9A2A-8D17A48B7842"), InterfaceType(ComInterfaceType.InterfaceIsIUnknown)] + internal interface ISpRecoResult : ISpPhrase + { + // ISpPhrase Methods + new void GetPhrase(out IntPtr ppCoMemPhrase); + new void GetSerializedPhrase(out IntPtr ppCoMemPhrase); + new void GetText(uint ulStart, uint ulCount, [MarshalAs(UnmanagedType.Bool)] bool fUseTextReplacements, [MarshalAs(UnmanagedType.LPWStr)] out string ppszCoMemText, out byte pbDisplayAttributes); + new void Discard(uint dwValueTypes); + + // ISpRecoResult Methods + void Slot5(); // void GetResultTimes(out SPRECORESULTTIMES pTimes); + void GetAlternates(int ulStartElement, int cElements, int ulRequestCount, [MarshalAs(UnmanagedType.LPArray, SizeParamIndex = 2), Out] IntPtr[] ppPhrases, out int pcPhrasesReturned); + void GetAudio(uint ulStartElement, uint cElements, out ISpStreamFormat ppStream); + void Slot8(); // void SpeakAudio(UInt32 ulStartElement, UInt32 cElements, UInt32 dwFlags, out UInt32 pulStreamNumber); + void Serialize(out IntPtr ppCoMemSerializedResult); + void Slot10(); // void ScaleAudio(ref Guid pAudioFormatId, IntPtr pWaveFormatEx); + void Slot11(); // void GetRecoContext(out ISpRecoContext ppRecoContext); + } + + [ComImport, Guid("8FCEBC98-4E49-4067-9C6C-D86A0E092E3D"), InterfaceType(ComInterfaceType.InterfaceIsIUnknown)] + internal interface ISpPhraseAlt : ISpPhrase + { + // ISpPhrase Methods + new void GetPhrase(out IntPtr ppCoMemPhrase); + new void GetSerializedPhrase(out IntPtr ppCoMemPhrase); + new void GetText(uint ulStart, uint ulCount, [MarshalAs(UnmanagedType.Bool)] bool fUseTextReplacements, [MarshalAs(UnmanagedType.LPWStr)] out string ppszCoMemText, out byte pbDisplayAttributes); + new void Discard(uint dwValueTypes); + + // ISpPhraseAlt Methods + void GetAltInfo(out ISpPhrase ppParent, out uint pulStartElementInParent, out uint pcElementsInParent, out uint pcElementsInAlt); + void Commit(); + } + + [ComImport, Guid("27CAC6C4-88F2-41f2-8817-0C95E59F1E6E"), InterfaceType(ComInterfaceType.InterfaceIsIUnknown)] + internal interface ISpRecoResult2 : ISpRecoResult + { + // ISpPhrase Methods + new void GetPhrase(out IntPtr ppCoMemPhrase); + new void GetSerializedPhrase(out IntPtr ppCoMemPhrase); + new void GetText(uint ulStart, uint ulCount, [MarshalAs(UnmanagedType.Bool)] bool fUseTextReplacements, [MarshalAs(UnmanagedType.LPWStr)] out string ppszCoMemText, out byte pbDisplayAttributes); + new void Discard(uint dwValueTypes); + + // ISpRecoResult Methods + new void Slot5(); // new void GetResultTimes(out SPRECORESULTTIMES pTimes); + new void GetAlternates(int ulStartElement, int cElements, int ulRequestCount, [MarshalAs(UnmanagedType.LPArray, SizeParamIndex = 2), Out] IntPtr[] ppPhrases, out int pcPhrasesReturned); + new void GetAudio(uint ulStartElement, uint cElements, out ISpStreamFormat ppStream); + new void Slot8(); // void SpeakAudio(UInt32 ulStartElement, UInt32 cElements, UInt32 dwFlags, out UInt32 pulStreamNumber); + new void Serialize(out IntPtr ppCoMemSerializedResult); + new void Slot10(); // void ScaleAudio(ref Guid pAudioFormatId, IntPtr pWaveFormatEx); + new void Slot11(); // void GetRecoContext(out ISpRecoContext ppRecoContext); + + // ISpRecoResult2 Methods + void CommitAlternate(ISpPhraseAlt pPhraseAlt, out ISpRecoResult ppNewResult); + void CommitText(uint ulStartElement, uint ulCountOfElements, [MarshalAs(UnmanagedType.LPWStr)] string pszCorrectedData, SPCOMMITFLAGS commitFlags); + void SetTextFeedback([MarshalAs(UnmanagedType.LPWStr)] string pszFeedback, [MarshalAs(UnmanagedType.Bool)] bool fSuccessful); + } + + [ComImport, Guid("AE39362B-45A8-4074-9B9E-CCF49AA2D0B6"), InterfaceType(ComInterfaceType.InterfaceIsIUnknown)] + internal interface ISpXMLRecoResult : ISpRecoResult + { + // ISpPhrase Methods + new void GetPhrase(out IntPtr ppCoMemPhrase); + new void GetSerializedPhrase(out IntPtr ppCoMemPhrase); + new void GetText(uint ulStart, uint ulCount, [MarshalAs(UnmanagedType.Bool)] bool fUseTextReplacements, [MarshalAs(UnmanagedType.LPWStr)] out string ppszCoMemText, out byte pbDisplayAttributes); + new void Discard(uint dwValueTypes); + + // ISpRecoResult Methods + new void Slot5(); // new void GetResultTimes(out SPRECORESULTTIMES pTimes); + new void GetAlternates(int ulStartElement, int cElements, int ulRequestCount, [MarshalAs(UnmanagedType.LPArray, SizeParamIndex = 2), Out] IntPtr[] ppPhrases, out int pcPhrasesReturned); + new void GetAudio(uint ulStartElement, uint cElements, out ISpStreamFormat ppStream); + new void Slot8(); // void SpeakAudio(UInt32 ulStartElement, UInt32 cElements, UInt32 dwFlags, out UInt32 pulStreamNumber); + new void Serialize(out IntPtr ppCoMemSerializedResult); + new void Slot10(); // void ScaleAudio(ref Guid pAudioFormatId, IntPtr pWaveFormatEx); + new void Slot11(); // void GetRecoContext(out ISpRecoContext ppRecoContext); + + // ISpXMLRecoResult Methods + void GetXMLResult([MarshalAs(UnmanagedType.LPWStr)] out string ppszCoMemXMLResult, SPXMLRESULTOPTIONS Options); + void GetXMLErrorInfo(out SPSEMANTICERRORINFO pSemanticErrorInfo); + } + + [ComImport, Guid("F264DA52-E457-4696-B856-A737B717AF79"), InterfaceType(ComInterfaceType.InterfaceIsIUnknown)] + internal interface ISpPhraseEx : ISpPhrase + { + // ISpPhrase Methods + new void GetPhrase(out IntPtr ppCoMemPhrase); + new void GetSerializedPhrase(out IntPtr ppCoMemPhrase); + new void GetText(uint ulStart, uint ulCount, [MarshalAs(UnmanagedType.Bool)] bool fUseTextReplacements, [MarshalAs(UnmanagedType.LPWStr)] out string ppszCoMemText, out byte pbDisplayAttributes); + new void Discard(uint dwValueTypes); + + // ISpPhraseEx Methods + void GetXMLResult([MarshalAs(UnmanagedType.LPWStr)] out string ppszCoMemXMLResult, SPXMLRESULTOPTIONS Options); + void GetXMLErrorInfo(out SPSEMANTICERRORINFO pSemanticErrorInfo); + void Slot7(); // void GetAudio(UInt32 ulStartElement, UInt32 cElements, out ISpStreamFormat ppStream); + } + + [ComImport, Guid("C8D7C7E2-0DDE-44b7-AFE3-B0C991FBEB5E"), InterfaceType(ComInterfaceType.InterfaceIsIUnknown)] + internal interface ISpDisplayAlternates + { + void GetDisplayAlternates(IntPtr pPhrase, uint cRequestCount, [MarshalAs(UnmanagedType.LPArray, SizeParamIndex = 2), Out] IntPtr[] ppCoMemPhrases, out uint pcPhrasesReturned); + } + + /// + /// Resource Loader interface definition + /// + [ComImport, Guid("B9AC5783-FCD0-4b21-B119-B4F8DA8FD2C3")] + internal interface ISpGrammarResourceLoader + { + /// + /// Load some data + /// + [PreserveSig] + int LoadResource(string bstrResourceUri, bool fAlwaysReload, out IStream pStream, ref string pbstrMIMEType, ref short pfModified, ref string pbstrRedirectUrl); + + /// + /// Converts the resourcePath to a location in the file cache and returns a reference into the + /// cache + /// + string GetLocalCopy(Uri resourcePath, out string mimeType, out Uri redirectUrl); + + /// + /// Mark an entry in the file cache as unused. + /// + void ReleaseLocalCopy(string path); + } + + [ComImport, Guid("88A3342A-0BED-4834-922B-88D43173162F"), InterfaceType(ComInterfaceType.InterfaceIsIUnknown)] + internal interface ISpPhraseBuilder : ISpPhrase + { + // ISpPhrase Methods + new void GetPhrase(out IntPtr ppCoMemPhrase); + new void GetSerializedPhrase(out IntPtr ppCoMemPhrase); + new void GetText(uint ulStart, uint ulCount, [MarshalAs(UnmanagedType.Bool)] bool fUseTextReplacements, [MarshalAs(UnmanagedType.LPWStr)] out string ppszCoMemText, out byte pbDisplayAttributes); + new void Discard(uint dwValueTypes); + + void InitFromPhrase(SPPHRASE pPhrase); + void Slot6(); // InitFromSerializedPhrase(const SPSERIALIZEDPHRASE * pPhrase); + void Slot7(); // AddElements(ULONG cElements, const SPPHRASEELEMENT *pElement); + void Slot8(); // AddRules(const SPPHRASERULEHANDLE hParent, const SPPHRASERULE * pRule, SPPHRASERULEHANDLE * phNewRule); + void Slot9(); // AddProperties(const SPPHRASEPROPERTYHANDLE hParent, const SPPHRASEPROPERTY * pProperty, SPPHRASEPROPERTYHANDLE * phNewProperty); + void Slot10(); // AddReplacements(ULONG cReplacements, const SPPHRASEREPLACEMENT * pReplacements); + }; + + #endregion + + #region Class + + [ComImport, Guid("3BEE4890-4FE9-4A37-8C1E-5E7E12791C1F")] + internal class SpSharedRecognizer { } + + [ComImport, Guid("41B89B6B-9399-11D2-9623-00C04F8EE628")] + internal class SpInprocRecognizer { } + + [ComImport, Guid("777B6BBD-2FF2-11D3-88FE-00C04F8EF9B5")] + internal class SpPhraseBuilder { } + + #endregion Class +} diff --git a/src/libraries/System.Speech/src/Internal/SapiInterop/SapiRecognizer.cs b/src/libraries/System.Speech/src/Internal/SapiInterop/SapiRecognizer.cs new file mode 100644 index 00000000000000..8fc8c603997417 --- /dev/null +++ b/src/libraries/System.Speech/src/Internal/SapiInterop/SapiRecognizer.cs @@ -0,0 +1,263 @@ +// 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.Runtime.InteropServices; +using System.Speech.Internal.ObjectTokens; +using System.Speech.Recognition; + +namespace System.Speech.Internal.SapiInterop +{ + internal class SapiRecognizer : IDisposable + { + #region Constructors + + internal SapiRecognizer(RecognizerType type) + { + ISpRecognizer recognizer; + try + { + if (type == RecognizerType.InProc) + { + recognizer = (ISpRecognizer)new SpInprocRecognizer(); + } + else + { + recognizer = (ISpRecognizer)new SpSharedRecognizer(); + } + _isSap53 = recognizer is ISpRecognizer2; + } + catch (COMException e) + { + throw RecognizerBase.ExceptionFromSapiCreateRecognizerError(e); + } + + // Back out if the recognizer we have SAPI 5.1 + if (!IsSapi53 && System.Threading.Thread.CurrentThread.GetApartmentState() == System.Threading.ApartmentState.STA) + { + // must be recreated on a different thread + Marshal.ReleaseComObject(recognizer); + _proxy = new SapiProxy.MTAThread(type); + } + else + { + _proxy = new SapiProxy.PassThrough(recognizer); + } + } + + public void Dispose() + { + if (!_disposed) + { + _proxy.Dispose(); + _disposed = true; + } + GC.SuppressFinalize(this); + } + + #endregion + + #region Internal Methods + + // ISpProperties Methods + internal void SetPropertyNum(string name, int value) + { + _proxy.Invoke2(delegate { SetProperty(_proxy.Recognizer, name, value); }); + } + + internal int GetPropertyNum(string name) + { + return (int)_proxy.Invoke(delegate { return GetProperty(_proxy.Recognizer, name, true); }); + } + internal void SetPropertyString(string name, string value) + { + _proxy.Invoke2(delegate { SetProperty(_proxy.Recognizer, name, value); }); + } + + internal string GetPropertyString(string name) + { + return (string)_proxy.Invoke(delegate { return GetProperty(_proxy.Recognizer, name, false); }); + } + + // ISpRecognizer Methods + internal void SetRecognizer(ISpObjectToken recognizer) + { + try + { + _proxy.Invoke2(delegate { _proxy.Recognizer.SetRecognizer(recognizer); }); + } + catch (InvalidCastException) + { + // The Interop layer maps the SAPI error that an interface cannot by QI to an Invalid cast exception + // Map the InvalidCastException + throw new PlatformNotSupportedException(SR.Get(SRID.NotSupportedWithThisVersionOfSAPI)); + } + } + + internal RecognizerInfo GetRecognizerInfo() + { + ISpObjectToken sapiObjectToken; + return (RecognizerInfo)_proxy.Invoke(delegate + { + RecognizerInfo recognizerInfo; + _proxy.Recognizer.GetRecognizer(out sapiObjectToken); + + IntPtr sapiTokenId; + try + { + sapiObjectToken.GetId(out sapiTokenId); + string tokenId = Marshal.PtrToStringUni(sapiTokenId); + recognizerInfo = RecognizerInfo.Create(ObjectToken.Open(null, tokenId, false)); + if (recognizerInfo == null) + { + throw new InvalidOperationException(SR.Get(SRID.RecognizerNotFound)); + } + Marshal.FreeCoTaskMem(sapiTokenId); + } + finally + { + Marshal.ReleaseComObject(sapiObjectToken); + } + + return recognizerInfo; + }); + } + + internal void SetInput(object input, bool allowFormatChanges) + { + _proxy.Invoke2(delegate { _proxy.Recognizer.SetInput(input, allowFormatChanges); }); + } + + internal SapiRecoContext CreateRecoContext() + { + ISpRecoContext context; + return (SapiRecoContext)_proxy.Invoke(delegate { _proxy.Recognizer.CreateRecoContext(out context); return new SapiRecoContext(context, _proxy); }); + } + + internal SPRECOSTATE GetRecoState() + { + SPRECOSTATE state; + return (SPRECOSTATE)_proxy.Invoke(delegate { _proxy.Recognizer.GetRecoState(out state); return state; }); + } + + internal void SetRecoState(SPRECOSTATE state) + { + _proxy.Invoke2(delegate { _proxy.Recognizer.SetRecoState(state); }); + } + + internal SPRECOGNIZERSTATUS GetStatus() + { + SPRECOGNIZERSTATUS status; + return (SPRECOGNIZERSTATUS)_proxy.Invoke(delegate { _proxy.Recognizer.GetStatus(out status); return status; }); + } + + internal IntPtr GetFormat(SPSTREAMFORMATTYPE WaveFormatType) + { + return (IntPtr)_proxy.Invoke(delegate + { + Guid formatId; + IntPtr ppCoMemWFEX; + _proxy.Recognizer.GetFormat(WaveFormatType, out formatId, out ppCoMemWFEX); return ppCoMemWFEX; + }); + } + + internal SAPIErrorCodes EmulateRecognition(string phrase) + { + object displayAttributes = " "; // Passing a null object here doesn't work because EmulateRecognition doesn't handle VT_EMPTY + return (SAPIErrorCodes)_proxy.Invoke(delegate { return _proxy.SapiSpeechRecognizer.EmulateRecognition(phrase, ref displayAttributes, 0); }); + } + + internal SAPIErrorCodes EmulateRecognition(ISpPhrase iSpPhrase, uint dwCompareFlags) + { + return (SAPIErrorCodes)_proxy.Invoke(delegate { return _proxy.Recognizer2.EmulateRecognitionEx(iSpPhrase, dwCompareFlags); }); + } + + #endregion + + #region Internal Properties + + internal bool IsSapi53 + { + get + { + return _isSap53; + } + } + + #endregion + + #region Internal Types + + internal enum RecognizerType + { + InProc, + Shared + } + + #endregion + + #region Private Methods + + private static void SetProperty(ISpRecognizer sapiRecognizer, string name, object value) + { + SAPIErrorCodes errorCode; + + if (value is int) + { + errorCode = (SAPIErrorCodes)sapiRecognizer.SetPropertyNum(name, (int)value); + } + else + { + errorCode = (SAPIErrorCodes)sapiRecognizer.SetPropertyString(name, (string)value); + } + + if (errorCode == SAPIErrorCodes.S_FALSE) + { + throw new KeyNotFoundException(SR.Get(SRID.RecognizerSettingNotSupported)); + } + else if (errorCode < SAPIErrorCodes.S_OK) + { + throw RecognizerBase.ExceptionFromSapiCreateRecognizerError(new COMException(SR.Get(SRID.RecognizerSettingUpdateError), (int)errorCode)); + } + } + + private static object GetProperty(ISpRecognizer sapiRecognizer, string name, bool integer) + { + SAPIErrorCodes errorCode; + object result = null; + + if (integer) + { + int value; + errorCode = (SAPIErrorCodes)sapiRecognizer.GetPropertyNum(name, out value); + result = value; + } + else + { + string value; + errorCode = (SAPIErrorCodes)sapiRecognizer.GetPropertyString(name, out value); + result = value; + } + + if (errorCode == SAPIErrorCodes.S_FALSE) + { + throw new KeyNotFoundException(SR.Get(SRID.RecognizerSettingNotSupported)); + } + else if (errorCode < SAPIErrorCodes.S_OK) + { + throw RecognizerBase.ExceptionFromSapiCreateRecognizerError(new COMException(SR.Get(SRID.RecognizerSettingUpdateError), (int)errorCode)); + } + return result; + } + + #endregion + + #region Private Fields + + private SapiProxy _proxy; + private bool _disposed; + private bool _isSap53; + + #endregion + } +} diff --git a/src/libraries/System.Speech/src/Internal/SapiInterop/SapiStreamInterop.cs b/src/libraries/System.Speech/src/Internal/SapiInterop/SapiStreamInterop.cs new file mode 100644 index 00000000000000..2da4b823f9fe61 --- /dev/null +++ b/src/libraries/System.Speech/src/Internal/SapiInterop/SapiStreamInterop.cs @@ -0,0 +1,71 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Runtime.InteropServices; +using System.Runtime.InteropServices.ComTypes; +using STATSTG = System.Runtime.InteropServices.ComTypes.STATSTG; + +namespace System.Speech.Internal.SapiInterop +{ + #region enum + + internal enum SPFILEMODE + { + SPFM_OPEN_READONLY = 0, + SPFM_CREATE_ALWAYS = 3 + } + + #endregion Enum + + #region Interface + + [ComImport, Guid("BED530BE-2606-4F4D-A1C0-54C5CDA5566F"), InterfaceType(ComInterfaceType.InterfaceIsIUnknown)] + internal interface ISpStreamFormat : IStream + { + // ISequentialStream Methods + new void Read([MarshalAs(UnmanagedType.LPArray, SizeParamIndex = 1), Out] byte[] pv, int cb, IntPtr pcbRead); + new void Write([MarshalAs(UnmanagedType.LPArray, SizeParamIndex = 1)] byte[] pv, int cb, IntPtr pcbWritten); + + // IStream Methods + new void Seek(long dlibMove, int dwOrigin, IntPtr plibNewPosition); + new void SetSize(long libNewSize); + new void CopyTo(IStream pstm, long cb, IntPtr pcbRead, IntPtr pcbWritten); + new void Commit(int grfCommitFlags); + new void Revert(); + new void LockRegion(long libOffset, long cb, int dwLockType); + new void UnlockRegion(long libOffset, long cb, int dwLockType); + new void Stat(out STATSTG pstatstg, int grfStatFlag); + new void Clone(out IStream ppstm); + + // ISpStreamFormat Methods + void GetFormat(out Guid pguidFormatId, out IntPtr ppCoMemWaveFormatEx); + } + + [ComImport, Guid("BED530BE-2606-4F4D-A1C0-54C5CDA5566F"), InterfaceType(ComInterfaceType.InterfaceIsIUnknown)] + internal interface ISpStream : ISpStreamFormat + { + // ISequentialStream Methods + new void Read([MarshalAs(UnmanagedType.LPArray, SizeParamIndex = 1), Out] byte[] pv, int cb, IntPtr pcbRead); + new void Write([MarshalAs(UnmanagedType.LPArray, SizeParamIndex = 1)] byte[] pv, int cb, IntPtr pcbWritten); + // IStream Methods + new void Seek(long dlibMove, int dwOrigin, IntPtr plibNewPosition); + new void SetSize(long libNewSize); + new void CopyTo(IStream pstm, long cb, IntPtr pcbRead, IntPtr pcbWritten); + new void Commit(int grfCommitFlags); + new void Revert(); + new void LockRegion(long libOffset, long cb, int dwLockType); + new void UnlockRegion(long libOffset, long cb, int dwLockType); + new void Stat(out STATSTG pstatstg, int grfStatFlag); + new void Clone(out IStream ppstm); + // ISpStreamFormat Methods + new void GetFormat(out Guid pguidFormatId, out IntPtr ppCoMemWaveFormatEx); + + // ISpStream Methods + void SetBaseStream(IStream pStream, ref Guid rguidFormat, IntPtr pWaveFormatEx); + void Slot14(); // void GetBaseStream(IStream ** ppStream); + void BindToFile(string pszFileName, SPFILEMODE eMode, ref Guid pFormatId, IntPtr pWaveFormatEx, ulong ullEventInterest); + void Close(); + } + + #endregion +} diff --git a/src/libraries/System.Speech/src/Internal/SapiInterop/SpAudioStreamWrapper.cs b/src/libraries/System.Speech/src/Internal/SapiInterop/SpAudioStreamWrapper.cs new file mode 100644 index 00000000000000..106bc98c61e34d --- /dev/null +++ b/src/libraries/System.Speech/src/Internal/SapiInterop/SpAudioStreamWrapper.cs @@ -0,0 +1,181 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.IO; +using System.Runtime.InteropServices; +using System.Speech.AudioFormat; +using System.Speech.Internal.Synthesis; + +namespace System.Speech.Internal.SapiInterop +{ + internal class SpAudioStreamWrapper : SpStreamWrapper, ISpStreamFormat + { + #region Constructors + + internal SpAudioStreamWrapper(Stream stream, SpeechAudioFormatInfo audioFormat) : base(stream) + { + // Assume PCM to start with + _formatType = SAPIGuids.SPDFID_WaveFormatEx; + + if (audioFormat != null) + { + WAVEFORMATEX wfx = new(); + wfx.wFormatTag = (short)audioFormat.EncodingFormat; + wfx.nChannels = (short)audioFormat.ChannelCount; + wfx.nSamplesPerSec = audioFormat.SamplesPerSecond; + wfx.nAvgBytesPerSec = audioFormat.AverageBytesPerSecond; + wfx.nBlockAlign = (short)audioFormat.BlockAlign; + wfx.wBitsPerSample = (short)audioFormat.BitsPerSample; + wfx.cbSize = (short)audioFormat.FormatSpecificData().Length; + + _wfx = wfx.ToBytes(); + if (wfx.cbSize == 0) + { + byte[] wfxTemp = new byte[_wfx.Length + wfx.cbSize]; + Array.Copy(_wfx, wfxTemp, _wfx.Length); + Array.Copy(audioFormat.FormatSpecificData(), 0, wfxTemp, _wfx.Length, wfx.cbSize); + _wfx = wfxTemp; + } + } + else + { + try + { + GetStreamOffsets(stream); + } + catch (IOException) + { + throw new FormatException(SR.Get(SRID.SynthesizerInvalidWaveFile)); + } + } + } + + #endregion + + #region public Methods + + #region ISpStreamFormat interface implementation + + void ISpStreamFormat.GetFormat(out Guid guid, out IntPtr format) + { + guid = _formatType; + format = Marshal.AllocCoTaskMem(_wfx.Length); + Marshal.Copy(_wfx, 0, format, _wfx.Length); + } + + #endregion + + #endregion + + #region Internal Methods + +#pragma warning disable 56518 // The Binary reader cannot be disposed or it would close the underlying stream + + /// + /// Builds the + /// + internal void GetStreamOffsets(Stream stream) + { + BinaryReader br = new(stream); + // Read the riff Header + RIFFHDR riff = new(); + + riff._id = br.ReadUInt32(); + riff._len = br.ReadInt32(); + riff._type = br.ReadUInt32(); + + if (riff._id != RIFF_MARKER && riff._type != WAVE_MARKER) + { + throw new FormatException(); + } + + BLOCKHDR block = new(); + block._id = br.ReadUInt32(); + block._len = br.ReadInt32(); + + if (block._id != FMT_MARKER) + { + throw new FormatException(); + } + + // If the format is of type WAVEFORMAT then fake a cbByte with a length of zero + _wfx = br.ReadBytes(block._len); + + // Hardcode the value of the size for the structure element + // as the C# compiler pads the structure to the closest 4 or 8 bytes + if (block._len == 16) + { + byte[] wfxTemp = new byte[18]; + Array.Copy(_wfx, wfxTemp, 16); + _wfx = wfxTemp; + } + + while (true) + { + DATAHDR dataHdr = new(); + + // check for the end of file (+8 for the 2 DWORD) + if (stream.Position + 8 >= stream.Length) + { + break; + } + dataHdr._id = br.ReadUInt32(); + dataHdr._len = br.ReadInt32(); + + // Is this the WAVE data? + if (dataHdr._id == DATA_MARKER) + { + _endOfStreamPosition = stream.Position + dataHdr._len; + break; + } + else + { + // Skip this RIFF fragment. + stream.Seek(dataHdr._len, SeekOrigin.Current); + } + } + } + +#pragma warning restore 56518 // The Binary reader cannot be disposed or it would close the underlying stream + + #endregion + + #region Private Types + + private const uint RIFF_MARKER = 0x46464952; + private const uint WAVE_MARKER = 0x45564157; + private const uint FMT_MARKER = 0x20746d66; + private const uint DATA_MARKER = 0x61746164; + + [StructLayout(LayoutKind.Sequential)] + private struct RIFFHDR + { + internal uint _id; + internal int _len; /* file length less header */ + internal uint _type; /* should be "WAVE" */ + } + + [StructLayout(LayoutKind.Sequential)] + private struct BLOCKHDR + { + internal uint _id; /* should be "fmt " or "data" */ + internal int _len; /* block size less header */ + }; + + [StructLayout(LayoutKind.Sequential)] + private struct DATAHDR + { + internal uint _id; /* should be "fmt " or "data" */ + internal int _len; /* block size less header */ + } + + #endregion + + #region Private Fields + + private byte[] _wfx; + private Guid _formatType; + + #endregion + } +} diff --git a/src/libraries/System.Speech/src/Internal/SapiInterop/SpStreamWrapper.cs b/src/libraries/System.Speech/src/Internal/SapiInterop/SpStreamWrapper.cs new file mode 100644 index 00000000000000..fb39f446114977 --- /dev/null +++ b/src/libraries/System.Speech/src/Internal/SapiInterop/SpStreamWrapper.cs @@ -0,0 +1,118 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.IO; +using System.Runtime.InteropServices; +using System.Runtime.InteropServices.ComTypes; +using STATSTG = System.Runtime.InteropServices.ComTypes.STATSTG; + +namespace System.Speech.Internal.SapiInterop +{ + internal class SpStreamWrapper : IStream, IDisposable + { + #region Constructors + + internal SpStreamWrapper(Stream stream) + { + _stream = stream; + _endOfStreamPosition = stream.Length; + } + + public void Dispose() + { + _stream.Dispose(); + GC.SuppressFinalize(this); + } + + #endregion + + #region public Methods + + #region ISpStreamFormat interface implementation + + public void Read(byte[] pv, int cb, IntPtr pcbRead) + { + if (_endOfStreamPosition >= 0 && _stream.Position + cb > _endOfStreamPosition) + { + cb = (int)(_endOfStreamPosition - _stream.Position); + } + + int read = 0; + try + { + read = _stream.Read(pv, 0, cb); + } + catch (EndOfStreamException) + { + read = 0; + } + + if (pcbRead != IntPtr.Zero) + { + Marshal.WriteIntPtr(pcbRead, new IntPtr(read)); + } + } + + public void Write(byte[] pv, int cb, IntPtr pcbWritten) + { + throw new NotSupportedException(); + } + + public void Seek(long offset, int seekOrigin, IntPtr plibNewPosition) + { + _stream.Seek(offset, (SeekOrigin)seekOrigin); + + if (plibNewPosition != IntPtr.Zero) + { + Marshal.WriteIntPtr(plibNewPosition, new IntPtr(_stream.Position)); + } + } + public void SetSize(long libNewSize) + { + throw new NotSupportedException(); + } + public void CopyTo(IStream pstm, long cb, IntPtr pcbRead, IntPtr pcbWritten) + { + throw new NotSupportedException(); + } + public void Commit(int grfCommitFlags) + { + _stream.Flush(); + } + public void Revert() + { + throw new NotSupportedException(); + } + public void LockRegion(long libOffset, long cb, int dwLockType) + { + throw new NotSupportedException(); + } + public void UnlockRegion(long libOffset, long cb, int dwLockType) + { + throw new NotSupportedException(); + } + public void Stat(out STATSTG pstatstg, int grfStatFlag) + { + pstatstg = new STATSTG + { + cbSize = _stream.Length + }; + } + + public void Clone(out IStream ppstm) + { + throw new NotSupportedException(); + } + + #endregion + + #endregion + + #region Private Fields + + private Stream _stream; + protected long _endOfStreamPosition = -1; + + #endregion + } +} diff --git a/src/libraries/System.Speech/src/Internal/SapiInterop/SpeechEvent.cs b/src/libraries/System.Speech/src/Internal/SapiInterop/SpeechEvent.cs new file mode 100644 index 00000000000000..0758126ec01531 --- /dev/null +++ b/src/libraries/System.Speech/src/Internal/SapiInterop/SpeechEvent.cs @@ -0,0 +1,172 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Runtime.InteropServices; +using System.Speech.AudioFormat; + +namespace System.Speech.Internal.SapiInterop +{ + // Internal helper class that wraps a SAPI event structure. + // A new instance is created by calling SpeechEvent.TryCreateSpeechEvent + // Disposing this class will dispose all unmanaged memory. + internal class SpeechEvent : IDisposable + { + #region Constructors + + private SpeechEvent(SPEVENTENUM eEventId, SPEVENTLPARAMTYPE elParamType, + ulong ullAudioStreamOffset, IntPtr wParam, IntPtr lParam) + { + // We make a copy of the SPEVENTEX data but that's okay because the lParam will only be deleted once. + _eventId = eEventId; + _paramType = elParamType; + _audioStreamOffset = ullAudioStreamOffset; + _wParam = (ulong)wParam.ToInt64(); + _lParam = (ulong)lParam; + + // Let the GC know if we have a unmanaged object with a given size + if (_paramType == SPEVENTLPARAMTYPE.SPET_LPARAM_IS_POINTER || _paramType == SPEVENTLPARAMTYPE.SPET_LPARAM_IS_STRING) + { + GC.AddMemoryPressure(_sizeMemoryPressure = Marshal.SizeOf(_lParam)); + } + } + + private SpeechEvent(SPEVENT sapiEvent, SpeechAudioFormatInfo audioFormat) + : this(sapiEvent.eEventId, sapiEvent.elParamType, sapiEvent.ullAudioStreamOffset, sapiEvent.wParam, sapiEvent.lParam) + { + if (audioFormat == null || audioFormat.EncodingFormat == 0) + { + _audioPosition = TimeSpan.Zero; + } + else + { + _audioPosition = audioFormat.AverageBytesPerSecond > 0 ? new TimeSpan((long)((sapiEvent.ullAudioStreamOffset * TimeSpan.TicksPerSecond) / (ulong)audioFormat.AverageBytesPerSecond)) : TimeSpan.Zero; + } + } + + private SpeechEvent(SPEVENTEX sapiEventEx) : this(sapiEventEx.eEventId, sapiEventEx.elParamType, sapiEventEx.ullAudioStreamOffset, sapiEventEx.wParam, sapiEventEx.lParam) + { + _audioPosition = new TimeSpan((long)sapiEventEx.ullAudioTimeOffset); + } + + ~SpeechEvent() + { + Dispose(); + } + + public void Dispose() + { + // General code to free event data + if (_lParam != 0) + { + if (_paramType == SPEVENTLPARAMTYPE.SPET_LPARAM_IS_TOKEN || _paramType == SPEVENTLPARAMTYPE.SPET_LPARAM_IS_OBJECT) + { + Marshal.Release((IntPtr)_lParam); + } + else + { + if (_paramType == SPEVENTLPARAMTYPE.SPET_LPARAM_IS_POINTER || _paramType == SPEVENTLPARAMTYPE.SPET_LPARAM_IS_STRING) + { + Marshal.FreeCoTaskMem((IntPtr)_lParam); + } + } + + // Update the GC + if (_sizeMemoryPressure > 0) + { + GC.RemoveMemoryPressure(_sizeMemoryPressure); + _sizeMemoryPressure = 0; + } + + // Mark the object as being freed + _lParam = 0; + } + GC.SuppressFinalize(this); + } + + #endregion + + #region Internal Methods + + // This tries to get an event from the ISpEventSource. + // If there are no events queued then null is returned. + // Otherwise a new SpeechEvent is created and returned. + internal static SpeechEvent TryCreateSpeechEvent(ISpEventSource sapiEventSource, bool additionalSapiFeatures, SpeechAudioFormatInfo audioFormat) + { + uint fetched; + SpeechEvent speechEvent = null; + if (additionalSapiFeatures) + { + SPEVENTEX sapiEventEx; + ((ISpEventSource2)sapiEventSource).GetEventsEx(1, out sapiEventEx, out fetched); + if (fetched == 1) + { + speechEvent = new SpeechEvent(sapiEventEx); + } + } + else + { + SPEVENT sapiEvent; + sapiEventSource.GetEvents(1, out sapiEvent, out fetched); + if (fetched == 1) + { + speechEvent = new SpeechEvent(sapiEvent, audioFormat); + } + } + + return speechEvent; + } + + #endregion + + #region Internal Properties + + internal SPEVENTENUM EventId + { + get { return _eventId; } + } + internal ulong AudioStreamOffset + { + get { return _audioStreamOffset; } + } + + // The WParam is returned as a 64-bit value since unmanaged wParam is always 32 or 64 depending on architecture. + // This is always some kind of numeric value in SAPI - it is never a pointer that needs to freed. + internal ulong WParam + { + get { return _wParam; } + } + + internal ulong LParam + { + get { return _lParam; } + } + + internal TimeSpan AudioPosition + { + get { return _audioPosition; } + } + + #endregion + + #region Private Fields + + private SPEVENTENUM _eventId; + private SPEVENTLPARAMTYPE _paramType; + private ulong _audioStreamOffset; + private ulong _wParam; + private ulong _lParam; + private TimeSpan _audioPosition; + private int _sizeMemoryPressure; + + #endregion + } + + internal enum SPEVENTLPARAMTYPE : ushort + { + SPET_LPARAM_IS_UNDEFINED = 0x0000, + SPET_LPARAM_IS_TOKEN = 0x0001, + SPET_LPARAM_IS_OBJECT = 0x0002, + SPET_LPARAM_IS_POINTER = 0x0003, + SPET_LPARAM_IS_STRING = 0x0004 + } +} diff --git a/src/libraries/System.Speech/src/Internal/SeekableReadStream.cs b/src/libraries/System.Speech/src/Internal/SeekableReadStream.cs new file mode 100644 index 00000000000000..31a6a99d6ed180 --- /dev/null +++ b/src/libraries/System.Speech/src/Internal/SeekableReadStream.cs @@ -0,0 +1,245 @@ +// 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.Diagnostics; +using System.IO; + +namespace System.Speech.Internal +{ +#pragma warning disable 56528 // Override of Dispose(bool) not needed as base stream should not be closed. + + // Class that is used to wrap a stream that does not support Seek into one that does. + // While CacheDataForSeeking is true then Read data is buffered so that Seeking can be done later back into the buffer. + // The Read call will first use the buffer and then the actual data once the buffer is read. + // After CacheDataForSeeking is set to false data can be read from the buffer but no more Seeking can be done. + internal class SeekableReadStream : Stream + { + #region Constructors + + internal SeekableReadStream(Stream baseStream) + { + Debug.Assert(baseStream.CanRead); + + _canSeek = baseStream.CanSeek; // If the stream is already seekable then don't need to do anything special + _baseStream = baseStream; + } + + #endregion + + #region Internal Properties + + internal bool CacheDataForSeeking + { + set + { + // Currently we can switch the caching off, but not back on again. Not needed for current scenarios. + Debug.Assert(!value || _cacheDataForSeeking); + _cacheDataForSeeking = value; + } + } + + public override bool CanRead + { + get { return true; } + } + + public override bool CanSeek + { + get + { + // Can do seeking only if we are caching data or underlying stream supports it. + return (_canSeek || _cacheDataForSeeking); + } + } + + public override bool CanWrite + { + get { return false; } + } + + public override long Length + { + // Non Seekable streams may not implement this, but we don't have much choice as we can't calculate the Stream length any other way. + get { return _baseStream.Length; } + } + + public override long Position + { + get + { + if (_canSeek) + { + // Delegate to underlying Stream: + return _baseStream.Position; + } + else + { + return _virtualPosition; + } + } + set + { + if (_canSeek) + { + // Delegate to underlying Stream: + _baseStream.Position = value; + } + else if (value != _virtualPosition) + { + if (value < 0) + { + throw new ArgumentOutOfRangeException(nameof(value), SR.Get(SRID.MustBeGreaterThanZero)); + } + // We can't check the length here so you can Seek beyond the end of the Stream. This will error later though. + + if (_cacheDataForSeeking) + { + if (value < _buffer.Count) + { + // We're moving within the already buffered data so just move the position: + _virtualPosition = value; + } + else + { + // We're moving beyond current position. + // Thus Read the new data and buffer it. + + // Read until at new position: + long bytesToReadLong = value - _buffer.Count; + if (bytesToReadLong > int.MaxValue) + { + throw new NotSupportedException(SR.Get(SRID.SeekNotSupported)); + } + byte[] readBuffer = new byte[bytesToReadLong]; + Helpers.BlockingRead(_baseStream, readBuffer, 0, (int)bytesToReadLong); + + // Copy from readBuffer into cache: + _buffer.AddRange(readBuffer); + _virtualPosition = value; + } + } + else + { + // No longer caching data so we can't seek around. + // Limited cases of this could be supported if needed. + throw new NotSupportedException(SR.Get(SRID.SeekNotSupported)); + } + } + } + } + + #endregion + + #region Internal Methods + + public override int Read(byte[] buffer, int offset, int count) + { + if (_canSeek) + { + // Delegate to underlying Stream: + return _baseStream.Read(buffer, offset, count); + } + else + { + int bytesRead = 0; + if (_virtualPosition < _buffer.Count) + { + // if new position inside buffer then read until at end of buffer + int toCopy = (int)(_buffer.Count - _virtualPosition); + if (toCopy > count) + { + toCopy = count; + } + _buffer.CopyTo((int)_virtualPosition, buffer, offset, toCopy); + count -= toCopy; + _virtualPosition += toCopy; + offset += toCopy; + bytesRead += toCopy; + if (!_cacheDataForSeeking && _virtualPosition >= _buffer.Count) + { + // Used up all the buffer, free. + _buffer.Clear(); + } + } + if (count > 0) + { + // Still data to Read so read it from the base Stream: + int localBytesRead = _baseStream.Read(buffer, offset, count); + bytesRead += localBytesRead; + _virtualPosition += localBytesRead; + if (_cacheDataForSeeking) + { + // if caching then extend Stream. + _buffer.Capacity += localBytesRead; + // Copy from buffer + offset for bytesRead + for (int i = 0; i < localBytesRead; i++) + { + _buffer.Add(buffer[offset + i]); + } + } + // Even if we didn't read every requested byte we can return - that's the contract on Stream.Read. + } + return bytesRead; + } + } + + public override long Seek(long offset, SeekOrigin origin) + { + long position; + + checked // Check for integer overflow + { + switch (origin) + { + case SeekOrigin.Begin: + position = offset; + break; + + case SeekOrigin.Current: + position = Position + offset; + break; + + case SeekOrigin.End: + position = Length + offset; + break; + + default: + throw new ArgumentException(SR.Get(SRID.EnumInvalid, "SeekOrigin"), nameof(origin)); + } + } + + Position = position; // Actually update position, checks for out of range + return position; + } + + public override void SetLength(long value) + { + throw new NotSupportedException(SR.Get(SRID.SeekNotSupported)); + } + + public override void Write(byte[] buffer, int offset, int count) + { + throw new NotSupportedException(SR.Get(SRID.StreamMustBeWriteable)); + } + + public override void Flush() + { + _baseStream.Flush(); + } + + #endregion + + #region Private Fields + + private long _virtualPosition; + private List _buffer = new(); // Data cached from start of stream onwards. + + private Stream _baseStream; + private bool _cacheDataForSeeking = true; + private bool _canSeek; + + #endregion + } +#pragma warning restore 56528 +} diff --git a/src/libraries/System.Speech/src/Internal/SrgsCompiler/AppDomainGrammarProxy.cs b/src/libraries/System.Speech/src/Internal/SrgsCompiler/AppDomainGrammarProxy.cs new file mode 100644 index 00000000000000..c514d9aa7040f9 --- /dev/null +++ b/src/libraries/System.Speech/src/Internal/SrgsCompiler/AppDomainGrammarProxy.cs @@ -0,0 +1,316 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +#region Using directives + +using System.Globalization; +using System.Reflection; +using System.Speech.Recognition.SrgsGrammar; +using System.Text; + +#endregion + +#pragma warning disable 56500 // Remove all the catch all statements warnings used by the interop layer + +namespace System.Speech.Internal.SrgsCompiler +{ + internal class AppDomainGrammarProxy : MarshalByRefObject + { + internal SrgsRule[] OnInit(string method, object[] parameters, string onInitParameters, out Exception exceptionThrown) + { + exceptionThrown = null; + try + { + // If the onInitParameters are provided as a string, get the values as an array of value. + if (!string.IsNullOrEmpty(onInitParameters)) + { + parameters = MatchInitParameters(method, onInitParameters, _rule, _rule); + } + + // Find the constructor to call - there could be several + Type[] types = new Type[parameters != null ? parameters.Length : 0]; + + if (parameters != null) + { + for (int i = 0; i < parameters.Length; i++) + { + types[i] = parameters[i].GetType(); + } + } + + MethodInfo onInit = _grammarType.GetMethod(method, types); + + // If somehow we failed to find a constructor, let the system handle it + if (onInit == null) + { + throw new InvalidOperationException(SR.Get(SRID.ArgumentMismatch)); + } + + SrgsRule[] extraRules = null; + if (onInit != null) + { + extraRules = (SrgsRule[])onInit.Invoke(_grammar, parameters); + } + return extraRules; + } + catch (Exception e) + { + exceptionThrown = e; + return null; + } + } + + internal object OnRecognition(string method, object[] parameters, out Exception exceptionThrown) + { + exceptionThrown = null; + try + { + MethodInfo onRecognition = _grammarType.GetMethod(method, BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic); + + // Execute the parse routine + return onRecognition.Invoke(_grammar, parameters); + } + catch (Exception e) + { + exceptionThrown = e; + } + return null; + } + + internal object OnParse(string rule, string method, object[] parameters, out Exception exceptionThrown) + { + exceptionThrown = null; + try + { + MethodInfo onParse; + System.Speech.Recognition.Grammar grammar; + GetRuleInstance(rule, method, out onParse, out grammar); + + // Execute the parse routine + return onParse.Invoke(grammar, parameters); + } + catch (Exception e) + { + exceptionThrown = e; + return null; + } + } + + internal void OnError(string rule, string method, object[] parameters, out Exception exceptionThrown) + { + exceptionThrown = null; + try + { + MethodInfo onError; + System.Speech.Recognition.Grammar grammar; + GetRuleInstance(rule, method, out onError, out grammar); + + // Execute the parse routine + onError.Invoke(grammar, parameters); + } + catch (Exception e) + { + exceptionThrown = e; + } + } + + internal void Init(string rule, byte[] il, byte[] pdb) + { + _assembly = Assembly.Load(il, pdb); + + // Get the grammar class carrying the .NET Semantics code + _grammarType = GetTypeForRule(_assembly, rule); + + // Something is Wrong if the grammar class cannot be found + if (_grammarType == null) + { + throw new FormatException(SR.Get(SRID.RecognizerRuleNotFoundStream, rule)); + } + _rule = rule; + try + { + _grammar = (System.Speech.Recognition.Grammar)_assembly.CreateInstance(_grammarType.FullName); + } + catch (MissingMemberException) + { + throw new ArgumentException(SR.Get(SRID.RuleScriptInvalidParameters, _grammarType.FullName, rule), nameof(rule)); + } + } + + private void GetRuleInstance(string rule, string method, out MethodInfo onParse, out System.Speech.Recognition.Grammar grammar) + { + Type ruleClass = rule == _rule ? _grammarType : GetTypeForRule(_assembly, rule); + if (ruleClass == null || !ruleClass.IsSubclassOf(typeof(System.Speech.Recognition.Grammar))) + { + throw new FormatException(SR.Get(SRID.RecognizerInvalidBinaryGrammar)); + } + + try + { + grammar = ruleClass == _grammarType ? _grammar : (System.Speech.Recognition.Grammar)_assembly.CreateInstance(ruleClass.FullName); + } + catch (MissingMemberException) + { + throw new ArgumentException(SR.Get(SRID.RuleScriptInvalidParameters, ruleClass.FullName, rule), nameof(rule)); + } + onParse = grammar.MethodInfo(method); + } + + private static Type GetTypeForRule(Assembly assembly, string rule) + { + Type[] types = assembly.GetTypes(); + for (int iType = 0; iType < types.Length; iType++) + { + Type type = types[iType]; + if (type.Name == rule && type.IsPublic && type.IsSubclassOf(typeof(System.Speech.Recognition.Grammar))) + { + return type; + } + } + return null; + } + + /// + /// Construct a list of parameters from a sapi:params string. + /// + private object[] MatchInitParameters(string method, string onInitParameters, string grammar, string rule) + { + MethodInfo[] mis = _grammarType.GetMethods(); + + NameValuePair[] pairs = ParseInitParams(onInitParameters); + object[] values = new object[pairs.Length]; + bool foundConstructor = false; + for (int iCtor = 0; iCtor < mis.Length && !foundConstructor; iCtor++) + { + if (mis[iCtor].Name != method) + { + continue; + } + ParameterInfo[] paramInfo = mis[iCtor].GetParameters(); + + // Check if enough parameters are provided. + if (paramInfo.Length > pairs.Length) + { + continue; + } + foundConstructor = true; + for (int i = 0; i < pairs.Length && foundConstructor; i++) + { + NameValuePair pair = pairs[i]; + + // anonymous + if (pair._name == null) + { + values[i] = pair._value; + } + else + { + bool foundParameter = false; + for (int j = 0; j < paramInfo.Length; j++) + { + if (paramInfo[j].Name == pair._name) + { + values[j] = ParseValue(paramInfo[j].ParameterType, pair._value); + foundParameter = true; + break; + } + } + if (!foundParameter) + { + foundConstructor = false; + } + } + } + } + if (!foundConstructor) + { + throw new FormatException(SR.Get(SRID.CantFindAConstructor, grammar, rule, FormatConstructorParameters(mis, method))); + } + return values; + } + + /// + /// Parse the value for a type from a string to a strong type. + /// If the type does not support the Parse method then the operation fails. + /// + private static object ParseValue(Type type, string value) + { + if (type == typeof(string)) + { + return value; + } + return type.InvokeMember("Parse", BindingFlags.InvokeMethod, null, null, new object[] { value }, CultureInfo.InvariantCulture); + } + + /// + /// Returns the list of the possible parameter names and type for a grammar + /// + private static string FormatConstructorParameters(MethodInfo[] cis, string method) + { + StringBuilder sb = new(); + for (int iCtor = 0; iCtor < cis.Length; iCtor++) + { + if (cis[iCtor].Name == method) + { + sb.Append(sb.Length > 0 ? " or sapi:parms=\"" : "sapi:parms=\""); + ParameterInfo[] pis = cis[iCtor].GetParameters(); + for (int i = 0; i < pis.Length; i++) + { + if (i > 0) + { + sb.Append(';'); + } + ParameterInfo pi = pis[i]; + sb.Append(pi.Name); + sb.Append(':'); + sb.Append(pi.ParameterType.Name); + } + sb.Append('"'); + } + } + return sb.ToString(); + } + + /// + /// Split the init parameter strings into an array of name/values + /// The format must be "name:value". If the ':' then parameter is anonymous. + /// + private static NameValuePair[] ParseInitParams(string initParameters) + { + string[] parameters = initParameters.Split(new char[] { ';' }, StringSplitOptions.None); + NameValuePair[] pairs = new NameValuePair[parameters.Length]; + + for (int i = 0; i < parameters.Length; i++) + { + string parameter = parameters[i]; + int posColon = parameter.IndexOf(':'); + if (posColon >= 0) + { + pairs[i]._name = parameter.Substring(0, posColon); + pairs[i]._value = parameter.Substring(posColon + 1); + } + else + { + pairs[i]._value = parameter; + } + } + return pairs; + } + +#pragma warning disable 56524 // Arclist does not hold on any resources + + private System.Speech.Recognition.Grammar _grammar; + +#pragma warning restore 56524 // Arclist does not hold on any resources + + private Assembly _assembly; + private string _rule; + private Type _grammarType; + + private struct NameValuePair + { + internal string _name; + internal string _value; + } + } +} diff --git a/src/libraries/System.Speech/src/Internal/SrgsCompiler/Arc.cs b/src/libraries/System.Speech/src/Internal/SrgsCompiler/Arc.cs new file mode 100644 index 00000000000000..bedc7164ca8a3a --- /dev/null +++ b/src/libraries/System.Speech/src/Internal/SrgsCompiler/Arc.cs @@ -0,0 +1,875 @@ +// 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.Diagnostics; +using System.Globalization; +using System.Runtime.InteropServices; +using System.Speech.Internal.SrgsParser; +using System.Text; + +namespace System.Speech.Internal.SrgsCompiler +{ +#if DEBUG + [DebuggerDisplay("{ToString ()}")] +#endif + internal class Arc : IComparer, IComparable + { + #region Constructors + + internal Arc() + { + } + + internal Arc(Arc arc) + : this() + { + _start = arc._start; + _end = arc._end; + _iWord = arc._iWord; + _flWeight = arc._flWeight; + _confidence = arc._confidence; + _ruleRef = arc._ruleRef; + _specialTransitionIndex = arc._specialTransitionIndex; + _iSerialize = arc._iSerialize; + _matchMode = arc._matchMode; +#if DEBUG + _fCheckingForExitPath = arc._fCheckingForExitPath; + _be = arc._be; +#endif + } + + internal Arc(Arc arc, State start, State end) + : this(arc) + { + _start = start; + _end = end; + } + + internal Arc(Arc arc, State start, State end, int wordId) + : this(arc, start, end) + { + _iWord = wordId; + } + + internal Arc(string sWord, Rule ruleRef, StringBlob words, float flWeight, int confidence, Rule specialRule, MatchMode matchMode, ref bool fNeedWeightTable) + : this(sWord, ruleRef, words, flWeight, confidence, specialRule, s_serializeToken++, matchMode, ref fNeedWeightTable) + { + } + + private Arc(string sWord, Rule ruleRef, StringBlob words, float flWeight, int confidence, Rule specialRule, uint iSerialize, MatchMode matchMode, ref bool fNeedWeightTable) + : this(0, flWeight, confidence, 0, matchMode, ref fNeedWeightTable) + { + _ruleRef = ruleRef; + _iSerialize = iSerialize; + + if (ruleRef == null) + { + if (specialRule != null) + { + _specialTransitionIndex = (specialRule == CfgGrammar.SPRULETRANS_WILDCARD) ? CfgGrammar.SPWILDCARDTRANSITION : (specialRule == CfgGrammar.SPRULETRANS_DICTATION) ? CfgGrammar.SPDICTATIONTRANSITION : CfgGrammar.SPTEXTBUFFERTRANSITION; + } + else + { + words.Add(sWord, out _iWord); + } + } + } + + internal Arc(int iWord, float flWeight, int confidence, int ulSpecialTransitionIndex, MatchMode matchMode, ref bool fNeedWeightTable) + : this() + { + _confidence = confidence; + _iWord = iWord; + _flWeight = flWeight; + _matchMode = matchMode; + _iSerialize = s_serializeToken++; + + if (!flWeight.Equals(CfgGrammar.DEFAULT_WEIGHT)) + { + fNeedWeightTable |= true; + } + + _specialTransitionIndex = ulSpecialTransitionIndex; + } + + #endregion + + #region internal Methods + + #region IComparable Interface implementation + + public int CompareTo(Arc obj1) + { + return Compare(this, obj1); + } + + int IComparer.Compare(Arc obj1, Arc obj2) + { + return Compare(obj1, obj2); + } + + private int Compare(Arc obj1, Arc obj2) + { + if (obj1 == obj2) + return 0; + + if (obj1 == null) + return -1; + + if (obj2 == null) + return 1; + + Arc arc1 = obj1; + Arc arc2 = obj2; + int diff = arc1.SortRank() - arc2.SortRank(); + diff = diff != 0 ? diff : (int)arc1._iSerialize - (int)arc2._iSerialize; + + System.Diagnostics.Debug.Assert(diff != 0); + return diff; + } + + internal static int CompareContent(Arc arc1, Arc arc2) + { + // Compare arcs based on IndexOfWord, IsRuleRef, SpecialTransitionIndex, Optional, and RequiredConfidence. + // SemanticTag, StartState, EndState, Weight, and SerializeIndex are not factors. + if (arc1._iWord != arc2._iWord) + return arc1._iWord - arc2._iWord; + else + { + if (arc1._ruleRef != null || arc2._ruleRef != null || ((arc1._specialTransitionIndex - arc2._specialTransitionIndex) + (arc1._confidence - arc2._confidence) != 0)) + { + int diff = 0; + if (arc1._ruleRef != null || arc2._ruleRef != null) + { + if (arc1._ruleRef != null && arc2._ruleRef == null) + { + diff = -1; + } + else if (arc1._ruleRef == null && arc2._ruleRef != null) + { + diff = 1; + } + else + { + diff = string.Compare(arc1._ruleRef.Name, arc2._ruleRef.Name, StringComparison.CurrentCulture); + } + } + + if (diff != 0) + return diff; + else if (arc1._specialTransitionIndex != arc2._specialTransitionIndex) + return arc1._specialTransitionIndex - arc2._specialTransitionIndex; + else if (arc1._confidence != arc2._confidence) + return arc1._confidence - arc2._confidence; + } + // An identical match + return 0; + } + } + + internal static int CompareContentForKey(Arc arc1, Arc arc2) + { + int diff = CompareContent(arc1, arc2); + if (diff == 0) + { + return (int)arc1._iSerialize - (int)arc2._iSerialize; + } + return diff; + } + + #endregion + + internal float Serialize(StreamMarshaler streamBuffer, bool isLast, uint arcIndex) + { + CfgArc A = new(); + + A.LastArc = isLast; + A.HasSemanticTag = SemanticTagCount > 0; + A.NextStartArcIndex = (uint)(_end != null ? _end.SerializeId : 0); + if (_ruleRef != null) + { + A.RuleRef = true; + A.TransitionIndex = (uint)_ruleRef._iSerialize; //_pFirstState.SerializeId; + } + else + { + A.RuleRef = false; + if (_specialTransitionIndex != 0) + { + A.TransitionIndex = (uint)_specialTransitionIndex; + } + else + { + A.TransitionIndex = (uint)_iWord; + } + } + + A.LowConfRequired = (_confidence < 0); + A.HighConfRequired = (_confidence > 0); + A.MatchMode = (uint)_matchMode; + + // For new arcs SerializeId is INFINITE so we set it correctly here. + // For existing states we preserve the index from loading, + // unless new states have been added in, in which case the arc index, + // and hence the transition id have changed. There is a workaround in ReloadCmd + // to invalidate rules in this case. + _iSerialize = arcIndex; + + streamBuffer.WriteStream(A); + return _flWeight; + } + + internal static float SerializeExtraEpsilonWithTag(StreamMarshaler streamBuffer, Arc arc, bool isLast, uint arcIndex) + { + CfgArc A = new(); + + A.LastArc = isLast; + A.HasSemanticTag = true; + A.NextStartArcIndex = arcIndex; + A.TransitionIndex = 0; + + A.LowConfRequired = false; + A.HighConfRequired = false; + A.MatchMode = (uint)arc._matchMode; + + streamBuffer.WriteStream(A); + return arc._flWeight; + } + + internal void SetArcIndexForTag(int iArc, uint iArcOffset, bool tagsCannotSpanOverMultipleArcs) + { + _startTags[iArc]._cfgTag.StartArcIndex = iArcOffset; + _startTags[iArc]._cfgTag.ArcIndex = iArcOffset; + if (tagsCannotSpanOverMultipleArcs) + { + _startTags[iArc]._cfgTag.EndArcIndex = iArcOffset; + } + } + + internal void SetEndArcIndexForTags() + { + if (_endTags != null) + { + foreach (Tag tag in _endTags) + { + tag._cfgTag.EndArcIndex = _iSerialize; + } + } + } + + /// + /// Compare the contents and number of output arcs from the start state. + /// The comparison is done by Arc content, number of arcs at then and the id for the last arc + /// + internal static int CompareForDuplicateInputTransitions(Arc arc1, Arc arc2) + { + int iContentCompare = Arc.CompareContent(arc1, arc2); + + if (iContentCompare != 0) + { + return iContentCompare; + } + + // Compare by arc Id + return (int)(arc1._start != null ? arc1._start.Id : 0) - (int)(arc2._start != null ? arc2._start.Id : 0); + } + + /// + /// Compare the contents and number of input arcs to the end state. + /// The comparison is done by Arc content, number of arcs at then and the id for the last arc + /// + internal static int CompareForDuplicateOutputTransitions(Arc arc1, Arc arc2) + { + // Compare content and number of other input transitions to the end state. + int iContentCompare = Arc.CompareContent(arc1, arc2); + + if (iContentCompare != 0) + { + return iContentCompare; + } + + // Compare by arc Id + return (int)(arc1._end != null ? arc1._end.Id : 0) - (int)(arc2._end != null ? arc2._end.Id : 0); + } + + /// + /// Compare the contents and start/end states of two arcs. + /// + internal static int CompareIdenticalTransitions(Arc arc1, Arc arc2) + { + // Same start arc + int diff = (int)(arc1._start != null ? arc1._start.Id : 0) - (int)(arc2._start != null ? arc2._start.Id : 0); + if (diff == 0) + { + // Same end arc + if ((diff = (int)(arc1._end != null ? arc1._end.Id : 0) - (int)(arc2._end != null ? arc2._end.Id : 0)) == 0) + { + // Same tag + diff = arc1.SameTags(arc2) ? 0 : 1; + } + } + return diff; + } + + internal void AddStartTag(Tag tag) + { + if (_startTags == null) + { + _startTags = new Collection(); + } + _startTags.Add(tag); + } + + internal void AddEndTag(Tag tag) + { + if (_endTags == null) + { + _endTags = new Collection(); + } + _endTags.Add(tag); + } + + internal void ClearTags() + { + _startTags = null; + _endTags = null; + } + + internal static void CopyTags(Arc src, Arc dest, Direction move) + { + // Copy the start tags if any + if (src._startTags != null) + { + // if dest has not tags just move the collection + if (dest._startTags == null) + { + dest._startTags = src._startTags; + } + else + { + if (move == Direction.Right) + { + for (int i = 0; i < src._startTags.Count; i++) + { + dest._startTags.Insert(i, src._startTags[i]); + } + } + else + { + // if dest has tags add the ones from the source to the existing ones + foreach (Tag tag in src._startTags) + { + dest._startTags.Add(tag); + } + } + } + } + + // Copy the end tags if any + if (src._endTags != null) + { + // if dest has not tags just move the collection + if (dest._endTags == null) + { + dest._endTags = src._endTags; + } + else + { + if (move == Direction.Right) + { + for (int i = 0; i < src._endTags.Count; i++) + { + dest._endTags.Insert(i, src._endTags[i]); + } + } + else + { + // if dest has tags add the ones from the source to the existing ones + foreach (Tag tag in src._endTags) + { + dest._endTags.Add(tag); + } + } + } + } + + // No tags src associated with the 'src' anymore + src._startTags = src._endTags = null; + } + + internal void CloneTags(Arc arc, List _tags, Dictionary endArcs, Backend be) + { + if (arc._startTags != null) + { + if (_startTags == null) + { + _startTags = new Collection(); + } + foreach (Tag tag in arc._startTags) + { + Tag newTag = new(tag); + _tags.Add(newTag); + _startTags.Add(newTag); + endArcs.Add(tag, newTag); +#if DEBUG + newTag._be = be; +#endif + if (be != null) + { + int idTagName; + newTag._cfgTag._nameOffset = be.Symbols.Add(tag._be.Symbols.FromOffset(tag._cfgTag._nameOffset), out idTagName); +#pragma warning disable 0618 // VarEnum is obsolete + if (tag._cfgTag._valueOffset != 0 && tag._cfgTag.PropVariantType == System.Runtime.InteropServices.VarEnum.VT_EMPTY) + { + newTag._cfgTag._valueOffset = be.Symbols.Add(tag._be.Symbols.FromOffset(tag._cfgTag._valueOffset), out idTagName); + } +#pragma warning restore 0618 + } + } + } + + if (arc._endTags != null) + { + if (_endTags == null) + { + _endTags = new Collection(); + } + foreach (Tag tag in arc._endTags) + { + Tag newTag = endArcs[tag]; + _endTags.Add(newTag); + endArcs.Remove(tag); + } + } + } + + internal bool SameTags(Arc arc) + { + // no tags ok + bool same = _startTags == null && arc._startTags == null; + + // Compare each tag if not null + if (!same && _startTags != null && arc._startTags != null && _startTags.Count == arc._startTags.Count) + { + same = true; + for (int i = 0; i < _startTags.Count; i++) + { + same &= _startTags[i] == arc._startTags[i]; + } + } + + // Compare end tags if the start tags are equal + if (same) + { + same = _endTags == null && arc._endTags == null; + + // Compare each tag if not null + if (!same && _endTags != null && arc._endTags != null && _endTags.Count == arc._endTags.Count) + { + same = true; + for (int i = 0; i < _endTags.Count; i++) + { + same &= _endTags[i] == arc._endTags[i]; + } + } + } + return same; + } + + internal void ConnectStates() + { + if (_end != null) + { + _end.InArcs.Add(this); + } + + if (_start != null) + { + _start.OutArcs.Add(this); + } + } + + /// + /// Is the arc an epsilon transition? + /// + internal bool IsEpsilonTransition + { + get + { + return (_ruleRef == null) && // Not a ruleref + (_specialTransitionIndex == 0) && // Not a special transition (wildcard, dictation, ...) + (_iWord == 0); // Not a word + } + } + + /// + /// Is this arc an arc without attached properties? + /// + /// Is this arc an arc without attached properties? + internal bool IsPropertylessTransition + { + get + { + // Does not own semantic property & No tag references + return _startTags == null && _endTags == null; + } + } + +#if DEBUG + + public override string ToString() + { + return (_start != null ? "#" + _start.Id.ToString(CultureInfo.InvariantCulture) : "") + " <- " + DebuggerDisplayTags() + " -> " + (_end != null ? "#" + _end.Id.ToString(CultureInfo.InvariantCulture) : ""); + } + + internal string DebuggerDisplayTags() + { + StringBuilder sb = new(); + if (_iWord == 0 && (_ruleRef != null || _specialTransitionIndex != 0)) + { + sb.Append('<'); + if (_ruleRef != null) + { + sb.Append(_ruleRef.Name); + } + else + { + switch (_specialTransitionIndex) + { + case CfgGrammar.SPWILDCARDTRANSITION: + sb.Append("GARBAGE"); + break; + + case CfgGrammar.SPTEXTBUFFERTRANSITION: + sb.Append("TEXTBUFFER"); + break; + + case CfgGrammar.SPDICTATIONTRANSITION: + sb.Append("DICTATION"); + break; + } + } + sb.Append('>'); + } + else + { + sb.Append('\''); + sb.Append(_iWord == 0 ? new string(new char[] { (char)0x3b5 }) : _be != null ? _be.Words[_iWord] : _iWord.ToString(CultureInfo.InvariantCulture)); + sb.Append('\''); + } + + if (_startTags != null || _endTags != null) + { + // Check if the tags are the same + bool same = _startTags != null && _endTags != null && _endTags.Count == _startTags.Count; + + // Compare each tag if not null + for (int i = 0; same && i < _endTags.Count; i++) + { + same &= _startTags[i] == _endTags[i]; + } + + sb.Append(" ("); + if (_startTags != null) + { + bool first = true; + foreach (Tag tag in _startTags) + { + if (!first) + { + sb.Append('|'); + } + sb.Append(GetSemanticTag(tag)); + first = false; + } + } + else + { + sb.Append('-'); + } + if (!same) + { + sb.Append(','); + if (_endTags != null) + { + bool first = true; + foreach (Tag tag in _endTags) + { + if (!first) + { + sb.Append('|'); + } + sb.Append(GetSemanticTag(tag)); + first = false; + } + } + else + { + sb.Append('-'); + } + } + sb.Append(')'); + } + return sb.ToString(); + } + +#endif + + #endregion + + #region internal Properties + + internal int SemanticTagCount + { + get + { + return _startTags == null ? 0 : _startTags.Count; + } + } + + internal State Start + { + get + { + return _start; + } + set + { + if (value != _start) + { + if (_start != null) + { + _start.OutArcs.Remove(this); + } + _start = value; + if (_start != null) + { + _start.OutArcs.Add(this); + } + } + } + } + + internal State End + { + get + { + return _end; + } + set + { + // If no change, then do nothing + if (value != _end) + { + if (_end != null) + { + _end.InArcs.Remove(this); + } + _end = value; + if (_end != null) + { + _end.InArcs.Add(this); + } + } + } + } + + internal int WordId + { + get + { + return _iWord; + } + } + + internal Rule RuleRef + { + get + { + return _ruleRef; + } + set + { + if ((_start != null && !_start.OutArcs.IsEmpty) || (_end != null && !_end.InArcs.IsEmpty)) + { + throw new InvalidOperationException(); + } + _ruleRef = value; + } + } + + internal float Weight + { + get + { + return _flWeight; + } + set + { + _flWeight = value; + } + } + + internal int SpecialTransitionIndex + { + get + { + return _specialTransitionIndex; + } + } + +#if DEBUG + internal bool CheckingForExitPath + { + get + { + return _fCheckingForExitPath; + } + set + { + _fCheckingForExitPath = value; + } + } + + internal Backend Backend + { + set + { + _be = value; + } + } +#endif + #endregion + + #region private Methods + +#if DEBUG + private string GetSemanticTag(Tag tag) + { + StringBuilder sb = new(); + string value; + string tagName = GetSemanticValue(tag._cfgTag, _be.Symbols, out value); + if (tagName != "SemanticKey") + { + if (tagName != "=") + { + sb.Append(tagName); + sb.Append('='); + } + sb.Append(value); + } + else + { + sb.Append('['); + sb.Append(value); + sb.Append(']'); + } + return sb.ToString(); + } + + private static string GetSemanticValue(CfgSemanticTag tag, StringBlob symbols, out string value) + { +#pragma warning disable 0618 // VarEnum is obsolete + switch (tag.PropVariantType) + { + case VarEnum.VT_EMPTY: + value = tag._valueOffset > 0 ? symbols.FromOffset(tag._valueOffset) : tag._valueOffset.ToString(CultureInfo.InvariantCulture); + break; + + case VarEnum.VT_I4: + case VarEnum.VT_UI4: + value = tag._varInt.ToString(CultureInfo.InvariantCulture); + break; + + case VarEnum.VT_R8: + value = tag._varDouble.ToString(CultureInfo.InvariantCulture); + break; + + case VarEnum.VT_BOOL: + value = tag._varInt == 0 ? "false" : "true"; + break; + + default: + value = "Unknown property type"; + break; + } +#pragma warning restore 0618 + + return tag._nameOffset > 0 ? symbols.FromOffset(tag._nameOffset) : tag._nameOffset.ToString(CultureInfo.InvariantCulture); ; + } +#endif + + // Sort arcs in a state based on type, and then on index. + // Arcs loaded from a file have their index preserved where possible. New dynamic states have index == INFINITE, + private int SortRank() + { + int ret = 0; + + if (_ruleRef != null) + ret = 0x1000000 + _ruleRef._cfgRule._nameOffset; // It's a rule - Place 2nd in list + + if (_iWord != 0) + ret += 0x2000000 + _iWord;// It's a word - Place last in list + + if (_specialTransitionIndex != 0) + ret += 0x3000000; // It's a special transition (dictation, text buffer, or wildcard) + + return ret; // It's an epsilon -- We're first + } + + #endregion + + #region Private Fields + + // Transition start state + private State _start; + + // Transition end state (or NULL for final state) + private State _end; + + // Either word index or pRule but not both + private int _iWord; + + // Rule ref + private Rule _ruleRef; + + // If != 0 then transition to dictation, text buffer, or wildcard + private int _specialTransitionIndex; + + private float _flWeight; + + // current matching mode + private MatchMode _matchMode; + + private int _confidence; + + // Index of arc in table when serialized. Recreated when we reload grammar. + + private uint _iSerialize; + + // If non-null then has semantic tag associated with this + private Collection _startTags; + private Collection _endTags; + + private static uint s_serializeToken = 1; + +#if DEBUG + // This is where the TransitionId comes from in engine interfaces. + private bool _fCheckingForExitPath; + private Backend _be; +#endif + + #endregion + } + + #region private Methods + + internal enum Direction + { + Right, + Left + } + #endregion +} diff --git a/src/libraries/System.Speech/src/Internal/SrgsCompiler/ArcList.cs b/src/libraries/System.Speech/src/Internal/SrgsCompiler/ArcList.cs new file mode 100644 index 00000000000000..849969d55b8b63 --- /dev/null +++ b/src/libraries/System.Speech/src/Internal/SrgsCompiler/ArcList.cs @@ -0,0 +1,92 @@ +// 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.Diagnostics; + +namespace System.Speech.Internal.SrgsCompiler +{ +#if DEBUG + [DebuggerDisplay("Count #{Count}")] + [DebuggerTypeProxy(typeof(ArcListDebugDisplay))] +#endif + internal class ArcList : RedBlackList + { + #region Internal Methods + + /// + /// Build a List with all the arcs + /// + internal List ToList() + { + List collection = new(); + foreach (Arc arc in this) + { + collection.Add(arc); + } + return collection; + } + + protected override int CompareTo(object arc1, object arc2) + { + return Arc.CompareContentForKey((Arc)arc1, (Arc)arc2); + } + + #endregion + + #region Internal Properties + + internal new Arc First + { + get + { + return (Arc)base.First; + } + } + + #endregion + + #region Private Members + +#if DEBUG + private int Count + { + get + { + int count = 0; + foreach (Arc arc in this) + { + count++; + } + return count; + } + } + + // Used by the debugger display attribute + private class ArcListDebugDisplay + { + public ArcListDebugDisplay(ArcList item) + { + _item = item; + } + [DebuggerBrowsable(DebuggerBrowsableState.RootHidden)] + public Arc[] AKeys + { + get + { + Arc[] item = new Arc[_item.Count]; + int i = 0; + foreach (Arc arc in _item) + { + item[i++] = arc; + } + return item; + } + } + + private ArcList _item; + } +#endif + #endregion + } +} diff --git a/src/libraries/System.Speech/src/Internal/SrgsCompiler/BackEnd.cs b/src/libraries/System.Speech/src/Internal/SrgsCompiler/BackEnd.cs new file mode 100644 index 00000000000000..a68dd999c7c50e --- /dev/null +++ b/src/libraries/System.Speech/src/Internal/SrgsCompiler/BackEnd.cs @@ -0,0 +1,1394 @@ +// 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.Globalization; +using System.Runtime.InteropServices; +using System.Speech.Internal.SrgsParser; +using System.Text; + +namespace System.Speech.Internal.SrgsCompiler +{ + internal sealed partial class Backend + { + #region Constructors + + internal Backend() + { + _words = new StringBlob(); + _symbols = new StringBlob(); + } + + internal Backend(StreamMarshaler streamHelper) + { + InitFromBinaryGrammar(streamHelper); + } + + #endregion + + #region Internal Methods + + /// + /// Optimizes the grammar network by removing the epsilon states and merging + /// duplicate transitions. + /// + internal void Optimize() + { + _states.Optimize(); + + // Most likely, there will be an arc with a weight != 1. So we need a weight table. + _fNeedWeightTable = true; + } + + /// + /// Performs consistency checks of the grammar structure, creates the + /// serialized format and either saves it to the stream provided by SetSaveOptions, + /// or reloads it into the CFG engine. + /// + internal void Commit(StreamMarshaler streamBuffer) + { + // For debugging purpose, assert if the position is not it is assumed it should be + // Keep the start position in the stream + long startStreamPostion = streamBuffer.Stream.Position; + + // put all states State into a sorted array by rule parent index and serialized index + List sortedStates = new(_states); + + // Release the memory for the original list of states + _states = null; + + sortedStates.Sort(); + + // Validate the grammar + ValidateAndTagRules(); + CheckLeftRecursion(sortedStates); + + // Include null terminator + int cBasePath = _basePath != null ? _basePath.Length + 1 : 0; + float[] pWeights; + int cArcs; + + // Add the top level semantic interpretation tag + // This should be set as the first symbol in the symbol string blog since it must hold on a 16 bits value. + int semanticInterpretationGlobals = 0; + if (_globalTags.Count > 0) + { + StringBuilder sb = new(); + foreach (string s in _globalTags) + { + sb.Append(s); + } + _symbols.Add(sb.ToString(), out semanticInterpretationGlobals); + semanticInterpretationGlobals = _symbols.OffsetFromId(semanticInterpretationGlobals); + if (semanticInterpretationGlobals > ushort.MaxValue) + { + throw new OverflowException(SR.Get(SRID.TooManyRulesWithSemanticsGlobals)); + } + } + + // Write the method names as symbols + foreach (ScriptRef script in _scriptRefs) + { + _symbols.Add(script._sMethod, out script._idSymbol); + } + // get the header + CfgGrammar.CfgSerializedHeader header = BuildHeader(sortedStates, cBasePath, unchecked((ushort)semanticInterpretationGlobals), out cArcs, out pWeights); + streamBuffer.WriteStream(header); + + // + // For the string blobs, we must explicitly report I/O error since the blobs don't + // use the error log facility. + // + System.Diagnostics.Debug.Assert(streamBuffer.Stream.Position - startStreamPostion == header.pszWords); + streamBuffer.WriteArrayChar(_words.SerializeData(), _words.SerializeSize()); + + System.Diagnostics.Debug.Assert(streamBuffer.Stream.Position - startStreamPostion == header.pszSymbols); + streamBuffer.WriteArrayChar(_symbols.SerializeData(), _symbols.SerializeSize()); + + System.Diagnostics.Debug.Assert(streamBuffer.Stream.Position - startStreamPostion == header.pRules); + foreach (Rule rule in _rules) + { + rule.Serialize(streamBuffer); + } + + if (cBasePath > 0) + { + streamBuffer.WriteArrayChar(_basePath.ToCharArray(), _basePath.Length); + + // Add a zero to be compatible with SAPI 5 + System.Diagnostics.Debug.Assert(_basePath.Length + 1 == cBasePath); + streamBuffer.WriteArrayChar(s_achZero, 1); + + // Zero-pad to align following structures + streamBuffer.WriteArray(s_abZero3, cBasePath * Helpers._sizeOfChar & 3); + } + + // + // Write a dummy 0 index state entry + // + CfgArc dummyArc = new(); + + System.Diagnostics.Debug.Assert(streamBuffer.Stream.Position - startStreamPostion == header.pArcs); + streamBuffer.WriteStream(dummyArc); + + int ulWeightOffset = 1; + uint arcOffset = 1; + + bool semanticInterpretation = (GrammarOptions & GrammarOptions.MssV1) == GrammarOptions.MssV1; + foreach (State state in sortedStates) + { + state.SerializeStateEntries(streamBuffer, semanticInterpretation, pWeights, ref arcOffset, ref ulWeightOffset); + } + + System.Diagnostics.Debug.Assert(streamBuffer.Stream.Position - startStreamPostion == header.pWeights); + if (_fNeedWeightTable) + { + streamBuffer.WriteArray(pWeights, cArcs); + } + + System.Diagnostics.Debug.Assert(streamBuffer.Stream.Position - startStreamPostion == header.tags); + if (!semanticInterpretation) + { + foreach (State state in sortedStates) + { + state.SetEndArcIndexForTags(); + } + } + + // Remove the orphaned arcs + // This could happen in the case of a + for (int i = _tags.Count - 1; i >= 0; i--) + { + // When arc are created the index is set to zero. This value changes during serialization + // if an arc references it + if (_tags[i]._cfgTag.ArcIndex == 0) + { + _tags.RemoveAt(i); + } + } + // Sort the _tags array by ArcIndex + _tags.Sort(); + + // Write the _tags array + foreach (Tag tag in _tags) + { + tag.Serialize(streamBuffer); + } + + // Write the script references and the IL write after the header so getting it for the grammar + // Does not require a seek to the end of the file + System.Diagnostics.Debug.Assert(header.pScripts == 0 || streamBuffer.Stream.Position - startStreamPostion == header.pScripts); + foreach (ScriptRef script in _scriptRefs) + { + script.Serialize(_symbols, streamBuffer); + } + + // Write the assembly bits + // (Not supported on this platform) + } + + /// + /// Description: + /// Combine the current data in a grammar with one coming from a CFG + /// + internal static Backend CombineGrammar(string ruleName, Backend org, Backend extra) + { + Backend be = new(); + be._fLoadedFromBinary = true; + be._fNeedWeightTable = org._fNeedWeightTable; + be._grammarMode = org._grammarMode; + be._grammarOptions = org._grammarOptions; + + // Hash source state to destination state + Dictionary srcToDestHash = new(); + + // Find the rule + foreach (Rule orgRule in org._rules) + { + if (orgRule.Name == ruleName) + { + be.CloneSubGraph(orgRule, org, extra, srcToDestHash, true); + } + } + return be; + } + + internal State CreateNewState(Rule rule) + { + return _states.CreateNewState(rule); + } + + internal void DeleteState(State state) + { + _states.DeleteState(state); + } + + internal void MoveInputTransitionsAndDeleteState(State from, State to) + { + _states.MoveInputTransitionsAndDeleteState(from, to); + } + + internal void MoveOutputTransitionsAndDeleteState(State from, State to) + { + _states.MoveOutputTransitionsAndDeleteState(from, to); + } + + /// + /// Tries to find the rule's initial state handle. If both a name and an id + /// are provided, then both have to match in order for this call to succeed. + /// If the rule doesn't already exist then we define it if fCreateIfNotExists, + /// otherwise we return an error (). + /// + /// - pszRuleName name of rule to find/define (null: don't care) + /// - ruleId id of rule to find/define (0: don't care) + /// - dwAttribute rule attribute for defining the rule + /// - fCreateIfNotExists creates the rule using name, id, and attributes + /// in case the rule doesn't already exist + /// + /// throws: + /// S_OK, E_INVALIDARG, E_OUTOFMEMORY + /// SPERR_RULE_NOT_FOUND -- no rule found and we don't create a new one + /// SPERR_RULE_NAME_ID_CONFLICT -- rule name and id don't match + /// + internal Rule CreateRule(string name, SPCFGRULEATTRIBUTES attributes) + { + + SPCFGRULEATTRIBUTES allFlags = SPCFGRULEATTRIBUTES.SPRAF_TopLevel | SPCFGRULEATTRIBUTES.SPRAF_Active | SPCFGRULEATTRIBUTES.SPRAF_Export | SPCFGRULEATTRIBUTES.SPRAF_Import | SPCFGRULEATTRIBUTES.SPRAF_Interpreter | SPCFGRULEATTRIBUTES.SPRAF_Dynamic | SPCFGRULEATTRIBUTES.SPRAF_Root; + + if (attributes != 0 && ((attributes & ~allFlags) != 0 || ((attributes & SPCFGRULEATTRIBUTES.SPRAF_Import) != 0 && (attributes & SPCFGRULEATTRIBUTES.SPRAF_Export) != 0))) + { + throw new ArgumentException(SR.Get(SRID.InvalidFlagsSet), nameof(attributes)); + } + + // SAPI does not properly handle a rule marked as Import and TopLevel/Active/Root. + // - To maintain maximal backwards compatibility, if a rule is marked as Import, we will unmark TopLevel/Active/Root. + // - This changes the behavior when application tries to activate this rule. However, given that it is already + // broken/fragile, we believe it is better to change the behavior. + if ((attributes & SPCFGRULEATTRIBUTES.SPRAF_Import) != 0 && ((attributes & SPCFGRULEATTRIBUTES.SPRAF_TopLevel) != 0 || (attributes & SPCFGRULEATTRIBUTES.SPRAF_Active) != 0 || (attributes & SPCFGRULEATTRIBUTES.SPRAF_Root) != 0)) + { + attributes &= ~(SPCFGRULEATTRIBUTES.SPRAF_TopLevel | SPCFGRULEATTRIBUTES.SPRAF_Active | SPCFGRULEATTRIBUTES.SPRAF_Root); + } + + if ((attributes & SPCFGRULEATTRIBUTES.SPRAF_Import) != 0 && (name[0] == '\0')) + { + LogError(name, SRID.InvalidImport); + } + + if (_fLoadedFromBinary) + { + // Scan all non-dynamic names and prevent a duplicate... + foreach (Rule r in _rules) + { + string wpszName = _symbols[r._cfgRule._nameOffset]; + + if (!r._cfgRule.Dynamic && name == wpszName) + { + LogError(name, SRID.DuplicatedRuleName); + } + } + } + + int idString; + int cImportedRule = 0; + Rule rule = new(this, name, _symbols.Add(name, out idString), attributes, _ruleIndex, 0, _grammarOptions & GrammarOptions.TagFormat, ref cImportedRule); + + rule._iSerialize2 = _ruleIndex++; + + if ((attributes & SPCFGRULEATTRIBUTES.SPRAF_Root) != 0) + { + if (_rootRule != null) + { + //We already have a root rule, return error code. + LogError(name, SRID.RootRuleAlreadyDefined); + } + else + { + _rootRule = rule; + } + } + + // Add rule to RuleListByName and RuleListByID hash tables. + if (rule._cfgRule._nameOffset != 0) + { + _nameOffsetRules.Add(rule._cfgRule._nameOffset, rule); + } + + // + // It is important to insert this at the tail for dynamic rules to + // retain their slot number. + // + _rules.Add(rule); + _rules.Sort(); + + return rule; + } + + /// + /// Internal method for finding rule in rule list + /// + internal Rule FindRule(string sRule) + { + Rule rule = null; + + if (_nameOffsetRules.Count > 0) + { + // Find rule corresponding to name symbol offset corresponding to the RuleName + int iWord = _symbols.Find(sRule); + + if (iWord > 0) + { + int dwSymbolOffset = _symbols.OffsetFromId(iWord); + + System.Diagnostics.Debug.Assert(dwSymbolOffset == 0 || _symbols[iWord] == sRule); + + rule = dwSymbolOffset > 0 && _nameOffsetRules.ContainsKey(dwSymbolOffset) ? _nameOffsetRules[dwSymbolOffset] : null; + } + } + + if (rule != null) + { + string sRuleFound = rule.Name; + + // at least one of the 2 arguments matched + // names either match or they are both null! + if (!((string.IsNullOrEmpty(sRule) || (!string.IsNullOrEmpty(sRule) && !string.IsNullOrEmpty(sRuleFound) && sRuleFound == sRule)))) + { + LogError(sRule, SRID.RuleNameIdConflict); + } + } + + return rule != null ? rule : null; + } + + /// + /// Adds a word transition from hFromState to hToState. If hToState == null + /// then the arc will be to the (implicit) terminal state. If psz == null then + /// we add an epsilon transition. Properties are pushed back to the + /// first un-ambiguous arc in case we can share a common initial state path. + /// The weight will be placed on the first arc (if there exists an arc with + /// the same word but different weight we will create a new arc). + /// + internal Arc WordTransition(string sWord, float flWeight, int requiredConfidence) + { + return CreateTransition(sWord, flWeight, requiredConfidence); + } + + internal Arc SubsetTransition(string text, MatchMode matchMode) + { + // Performs white space normalization in place + text = NormalizeTokenWhiteSpace(text); + + return new Arc(text, null, _words, 1.0f, CfgGrammar.SP_NORMAL_CONFIDENCE, null, matchMode, ref _fNeedWeightTable); + } + + /// + /// Adds a rule (reference) transition from hFromState to hToState. + /// hRule can also be one of these special transition handles: + /// SPRULETRANS_WILDCARD : "WILDCARD" transition + /// SPRULETRANS_DICTATION : single word from dictation + /// SPRULETRANS_TEXTBUFFER : "TEXTBUFFER" transition + /// + /// must be initial state of rule + /// Rule calling the ruleref + /// Weight + internal Arc RuleTransition(Rule rule, Rule parentRule, float flWeight) + { + Rule ruleToTransitionTo = null; + + if (flWeight < 0.0f) + { + XmlParser.ThrowSrgsException(SRID.UnsupportedFormat); + } + + Rule specialRuleTrans = null; + + if (rule == CfgGrammar.SPRULETRANS_WILDCARD || rule == CfgGrammar.SPRULETRANS_DICTATION || rule == CfgGrammar.SPRULETRANS_TEXTBUFFER) + { + specialRuleTrans = rule; + } + else + { + ruleToTransitionTo = rule; + } + + bool fNeedWeightTable = false; + Arc arc = new(null, ruleToTransitionTo, _words, flWeight, '\0', specialRuleTrans, MatchMode.AllWords, ref fNeedWeightTable); + + AddArc(arc); + + if (ruleToTransitionTo != null && parentRule != null) + { + ruleToTransitionTo._listRules.Insert(0, parentRule); + } + + return arc; + } + + /// + /// Adds a word transition from hFromState to hToState. If hToState == null + /// then the arc will be to the (implicit) terminal state. If psz == null then + /// we add an epsilon transition. Properties are pushed back to the + /// first un-ambiguous arc in case we can share a common initial state path. + /// The weight will be placed on the first arc (if there exists an arc with + /// the same word but different weight we will create a new arc). + /// + internal Arc EpsilonTransition(float flWeight) + { + return CreateTransition(null, flWeight, CfgGrammar.SP_NORMAL_CONFIDENCE); + } + + internal void AddSemanticInterpretationTag(Arc arc, CfgGrammar.CfgProperty propertyInfo) + { + + Tag tag = new(this, propertyInfo); + _tags.Add(tag); + + arc.AddStartTag(tag); + arc.AddEndTag(tag); + } + + internal void AddPropertyTag(Arc start, Arc end, CfgGrammar.CfgProperty propertyInfo) + { + + Tag tag = new(this, propertyInfo); + _tags.Add(tag); + + start.AddStartTag(tag); + end.AddEndTag(tag); + } + + /// + /// Traverse the graph starting from SrcStartState, cloning each state as we go along, + /// cloning each transition except ones originating from SrcEndState, and return + /// the cloned state corresponding to SrcEndState. + /// + internal State CloneSubGraph(State srcFromState, State srcEndState, State destFromState) + { + Dictionary SrcToDestHash = new(); // Hash source state to destination state + Stack CloneStack = new(); // States to process + Dictionary tags = new(); + + // Add initial state to CloneStack and SrcToDestHash. + SrcToDestHash.Add(srcFromState, destFromState); + CloneStack.Push(srcFromState); + + // While there are still states on the CloneStack (ToDo collection) + while (CloneStack.Count > 0) + { + srcFromState = CloneStack.Pop(); + destFromState = SrcToDestHash[srcFromState]; + System.Diagnostics.Debug.Assert(destFromState != null); + + // For each transition from srcFromState (except SrcEndState) + foreach (Arc arc in srcFromState.OutArcs) + { + // - Lookup the DestToState corresponding to SrcToState + State srcToState = arc.End; + State destToState = null; + + if (srcToState != null) + { + // - If not found, clone a new DestToState, add SrcToState.DestToState to SrcToDestHash, and add SrcToState to CloneStack. + if (!SrcToDestHash.ContainsKey(srcToState)) + { + destToState = CreateNewState(srcToState.Rule); + SrcToDestHash.Add(srcToState, destToState); + CloneStack.Push(srcToState); + } + else + { + destToState = SrcToDestHash[srcToState]; ; + } + } + + // - Clone the transition from SrcFromState.SrcToState at DestFromState.DestToState + // -- Clone Arc + Arc newArc = new(arc, destFromState, destToState); + AddArc(newArc); + + // -- Clone SemanticTag + newArc.CloneTags(arc, _tags, tags, null); + + // -- Add Arc + newArc.ConnectStates(); + } + } + + System.Diagnostics.Debug.Assert(tags.Count == 0); + return SrcToDestHash[srcEndState]; + } + + /// + /// Traverse the graph starting from SrcStartState, cloning each state as we go along, + /// cloning each transition except ones originating from SrcEndState, and return + /// the cloned state corresponding to SrcEndState. + /// + internal void CloneSubGraph(Rule rule, Backend org, Backend extra, Dictionary srcToDestHash, bool fromOrg) + { + Backend beSrc = fromOrg ? org : extra; + + List CloneStack = new(); // States to process + Dictionary tags = new(); + + // Push all the state for the top level rule + CloneState(rule._firstState, CloneStack, srcToDestHash); + + // While there are still states on the CloneStack (ToDo collection) + while (CloneStack.Count > 0) + { + State srcFromState = CloneStack[0]; + CloneStack.RemoveAt(0); + State destFromState = srcToDestHash[srcFromState]; + // For each transition from srcFromState (except SrcEndState) + foreach (Arc arc in srcFromState.OutArcs) + { + // - Lookup the DestToState corresponding to SrcToState + State srcToState = arc.End; + State destToState = null; + + if (srcToState != null) + { + if (!srcToDestHash.ContainsKey(srcToState)) + { + // - If not found, then it is a new rule, just clown it. + CloneState(srcToState, CloneStack, srcToDestHash); + } + destToState = srcToDestHash[srcToState]; + } + + // - Clone the transition from SrcFromState.SrcToState at DestFromState.DestToState + // -- Clone Arc + int newWordId = arc.WordId; + if (beSrc != null && arc.WordId > 0) + { + _words.Add(beSrc.Words[arc.WordId], out newWordId); + } + + Arc newArc = new(arc, destFromState, destToState, newWordId); + + // -- Clone SemanticTag + newArc.CloneTags(arc, _tags, tags, this); + + // For rule ref push the first state of the ruleref + if (arc.RuleRef != null) + { + string ruleName; + + // Check for DYNAMIC grammars + if (arc.RuleRef.Name.IndexOf("URL:DYNAMIC#", StringComparison.Ordinal) == 0) + { + ruleName = arc.RuleRef.Name.Substring(12); + if (fromOrg == true && FindInRules(ruleName) == null) + { + Rule ruleExtra = extra.FindInRules(ruleName); + if (ruleExtra == null) + { + XmlParser.ThrowSrgsException(SRID.DynamicRuleNotFound, ruleName); + } + CloneSubGraph(ruleExtra, org, extra, srcToDestHash, false); + } + } + else if (arc.RuleRef.Name.IndexOf("URL:STATIC#", StringComparison.Ordinal) == 0) + { + ruleName = arc.RuleRef.Name.Substring(11); + if (fromOrg == false && FindInRules(ruleName) == null) + { + Rule ruleOrg = org.FindInRules(ruleName); + if (ruleOrg == null) + { + XmlParser.ThrowSrgsException(SRID.DynamicRuleNotFound, ruleName); + } + CloneSubGraph(ruleOrg, org, extra, srcToDestHash, true); + } + } + else + { + ruleName = arc.RuleRef.Name; + Rule ruleExtra = org.FindInRules(ruleName); + if (fromOrg == false) + { + CloneSubGraph(arc.RuleRef, org, extra, srcToDestHash, true); + } + } + Rule refRule = FindInRules(ruleName); + if (refRule == null) + { + refRule = CloneState(arc.RuleRef._firstState, CloneStack, srcToDestHash); + } + newArc.RuleRef = refRule; + } + + // -- Add Arc + newArc.ConnectStates(); + } + } + System.Diagnostics.Debug.Assert(tags.Count == 0); + } + + /// + /// Delete disconnected subgraph starting at hState. + /// Traverse the graph starting from SrcStartState, and delete each state as we go along. + /// + internal void DeleteSubGraph(State state) + { + // Add initial state to DeleteStack. + Stack stateToProcess = new(); // States to delete + Collection arcsToDelete = new(); + Collection statesToDelete = new(); + stateToProcess.Push(state); + + // While there are still states on the listDelete (ToDo collection) + while (stateToProcess.Count > 0) + { + // For each transition from state, + state = stateToProcess.Pop(); + statesToDelete.Add(state); + arcsToDelete.Clear(); + + // Accumulate the arcs to delete and add new states to the stack of states to process + foreach (Arc arc in state.OutArcs) + { + // Add EndState to listDelete, if unique + State endState = arc.End; + + // Add this state to the list of states to delete + if (endState != null && !stateToProcess.Contains(endState) && !statesToDelete.Contains(endState)) + { + stateToProcess.Push(endState); + } + arcsToDelete.Add(arc); + } + // Clear up the arcs + foreach (Arc arc in arcsToDelete) + { + arc.Start = arc.End = null; + } + } + + foreach (State stateToDelete in statesToDelete) + { + // Delete state and remove from listDelete + System.Diagnostics.Debug.Assert(stateToDelete != null); + System.Diagnostics.Debug.Assert(stateToDelete.InArcs.IsEmpty); + System.Diagnostics.Debug.Assert(stateToDelete.OutArcs.IsEmpty); + DeleteState(stateToDelete); + } + } + + /// + /// Modify the placeholder rule attributes after it has been created. + /// This is only safe to use in the context of SrgsGrammarCompiler. + /// + internal void SetRuleAttributes(Rule rule, SPCFGRULEATTRIBUTES dwAttributes) + { + // Check if this is the Root rule + if ((dwAttributes & SPCFGRULEATTRIBUTES.SPRAF_Root) != 0) + { + if (_rootRule != null) + { + //We already have a root rule, return error code. + XmlParser.ThrowSrgsException(SRID.RootRuleAlreadyDefined); + } + else + { + _rootRule = rule; + } + } + + rule._cfgRule.TopLevel = ((dwAttributes & SPCFGRULEATTRIBUTES.SPRAF_TopLevel) != 0); + rule._cfgRule.DefaultActive = ((dwAttributes & SPCFGRULEATTRIBUTES.SPRAF_Active) != 0); + rule._cfgRule.PropRule = ((dwAttributes & SPCFGRULEATTRIBUTES.SPRAF_Interpreter) != 0); + rule._cfgRule.Export = ((dwAttributes & SPCFGRULEATTRIBUTES.SPRAF_Export) != 0); + rule._cfgRule.Dynamic = ((dwAttributes & SPCFGRULEATTRIBUTES.SPRAF_Dynamic) != 0); + rule._cfgRule.Import = ((dwAttributes & SPCFGRULEATTRIBUTES.SPRAF_Import) != 0); + } + + /// + /// Set the path from which relative grammar imports are calculated. As specified by xml:base / meta base + /// Null or empty string will clear any existing base path. + /// + internal void SetBasePath(string sBasePath) + { + if (!string.IsNullOrEmpty(sBasePath)) + { + // Validate base path. + Uri uri = new(sBasePath, UriKind.RelativeOrAbsolute); + + //Url Canonicalized + _basePath = uri.ToString(); + } + else + { + _basePath = null; + } + } + + /// + /// Perform white space normalization in place. + /// - Trim leading/trailing white spaces. + /// - Collapse white space sequences to a single ' '. + /// + internal static string NormalizeTokenWhiteSpace(string sToken) + { + System.Diagnostics.Debug.Assert(!string.IsNullOrEmpty(sToken)); + + // Trim leading and ending white spaces + sToken = sToken.Trim(Helpers._achTrimChars); + + // Easy out if there are no consecutive double white spaces + if (sToken.IndexOf(" ", StringComparison.Ordinal) == -1) + { + return sToken; + } + + // Normalize internal spaces + char[] achSrc = sToken.ToCharArray(); + int iDest = 0; + + for (int i = 0; i < achSrc.Length;) + { + // Collapsed multiple white spaces into ' ' + if (achSrc[i] == ' ') + { + do + { + i++; + } while (achSrc[i] == ' '); + + achSrc[iDest++] = ' '; + continue; + } + + // Copy the non-white space character + achSrc[iDest++] = achSrc[i++]; + } + + return new string(achSrc, 0, iDest); + } + + #endregion + + #region Internal Property + + internal StringBlob Words + { + get + { + return _words; + } + } + + internal StringBlob Symbols + { + get + { + return _symbols; + } + } + + #endregion + + #region Private Methods + + /// + /// Description: + /// Load compiled grammar data. This overwrites any existing data in the grammar + /// We end up with containers of words, symbols, rules, arcs, states and state handles, etc. + /// + internal void InitFromBinaryGrammar(StreamMarshaler streamHelper) + { + CfgGrammar.CfgHeader header = CfgGrammar.ConvertCfgHeader(streamHelper); + + _words = header.pszWords; + _symbols = header.pszSymbols; + + _grammarOptions = header.GrammarOptions; + + // + // Build up the internal representation + // + State[] apStateTable = new State[header.arcs.Length]; + SortedDictionary ruleFirstArcs = new(); + + // + // Initialize the rules + // + + int previousCfgLastRules = _rules.Count; + + BuildRulesFromBinaryGrammar(header, apStateTable, ruleFirstArcs, previousCfgLastRules); + + // + // Initialize the arcs + // + Arc[] apArcTable = new Arc[header.arcs.Length]; + bool fLastArcNull = true; + CfgArc pLastArc = new(); + State currentState = null; + IEnumerator> ieFirstArcs = ruleFirstArcs.GetEnumerator(); + + // If no rules, then we have no arcs + if (ieFirstArcs.MoveNext()) + { + KeyValuePair kvFirstArc = ieFirstArcs.Current; + Rule ruleCur = kvFirstArc.Value; + + // We repersist the static AND dynamic parts for now. This allows the grammar to be queried + // with the automation interfaces + for (int k = 1; k < header.arcs.Length; k++) + { + CfgArc arc = header.arcs[k]; + + // Reset the Transition index based on the combined string blobs + if (arc.RuleRef) + { + // for a ruleref offset the rule index + ruleCur._listRules.Add(_rules[(int)arc.TransitionIndex]); + } + + if (kvFirstArc.Key == k) + { + // we are entering a new rule now + ruleCur = kvFirstArc.Value; + + // Reset to zero once we have read the last rule. + if (ieFirstArcs.MoveNext()) + { + kvFirstArc = ieFirstArcs.Current; + } + } + + // new currentState? + if (fLastArcNull || pLastArc.LastArc) + { + if (apStateTable[k] == null) + { + uint hNewState = CfgGrammar.NextHandle; + + apStateTable[k] = new State(ruleCur, hNewState, k); + AddState(apStateTable[k]); + } + + currentState = apStateTable[k]; + } + + // + // now get the arc + // + int iNextArc = (int)(arc.NextStartArcIndex); + Arc newArc; + State targetState = null; + + if (currentState != null && iNextArc != 0) + { + if (apStateTable[iNextArc] == null) + { + uint hNewState = CfgGrammar.NextHandle; + + apStateTable[iNextArc] = new State(ruleCur, hNewState, iNextArc); + AddState(apStateTable[iNextArc]); + } + + targetState = apStateTable[iNextArc]; + } + + float flWeight = header.weights != null ? header.weights[k] : CfgGrammar.DEFAULT_WEIGHT; + + // determine properties of the arc now ... + if (arc.RuleRef) + { + Rule ruleToTransitionTo = _rules[(int)arc.TransitionIndex]; + + newArc = new Arc(null, ruleToTransitionTo, _words, flWeight, CfgGrammar.SP_NORMAL_CONFIDENCE, null, MatchMode.AllWords, ref _fNeedWeightTable); + } + else + { + int transitionIndex = (int)arc.TransitionIndex; + int ulSpecialTransitionIndex = (transitionIndex == CfgGrammar.SPWILDCARDTRANSITION || transitionIndex == CfgGrammar.SPDICTATIONTRANSITION || transitionIndex == CfgGrammar.SPTEXTBUFFERTRANSITION) ? transitionIndex : 0; + newArc = new Arc((ulSpecialTransitionIndex != 0) ? 0 : (int)arc.TransitionIndex, flWeight, arc.LowConfRequired ? CfgGrammar.SP_LOW_CONFIDENCE : arc.HighConfRequired ? CfgGrammar.SP_HIGH_CONFIDENCE : CfgGrammar.SP_NORMAL_CONFIDENCE, ulSpecialTransitionIndex, MatchMode.AllWords, ref _fNeedWeightTable); + } + newArc.Start = currentState; + newArc.End = targetState; + + AddArc(newArc); + apArcTable[k] = newArc; + fLastArcNull = false; + pLastArc = arc; + } + } + + // Initialize the Semantics tags + for (int k = 1, iCurTag = 0; k < header.arcs.Length; k++) + { + CfgArc arc = header.arcs[k]; + + if (arc.HasSemanticTag) + { + System.Diagnostics.Debug.Assert(header.tags[iCurTag].StartArcIndex == k); + + while (iCurTag < header.tags.Length && header.tags[iCurTag].StartArcIndex == k) + { + // we should already point to the tag + CfgSemanticTag semTag = header.tags[iCurTag]; + + Tag tag = new(this, semTag); + + _tags.Add(tag); + apArcTable[tag._cfgTag.StartArcIndex].AddStartTag(tag); + apArcTable[tag._cfgTag.EndArcIndex].AddEndTag(tag); + + // If we have ms-properties than _nameOffset != otherwise it is w3c tags. + if (semTag._nameOffset > 0) + { + tag._cfgTag._nameOffset = _symbols.OffsetFromId(_symbols.Find(_symbols.FromOffset(semTag._nameOffset))); + } + else + { + // The offset of the JScrip expression is stored in the value field. + tag._cfgTag._valueOffset = _symbols.OffsetFromId(_symbols.Find(_symbols.FromOffset(semTag._valueOffset))); + } + iCurTag++; + } + } + } + _fNeedWeightTable = true; + if (header.BasePath != null) + { + SetBasePath(header.BasePath); + } + + _guid = header.GrammarGUID; + _langId = header.langId; + _grammarMode = header.GrammarMode; + + _fLoadedFromBinary = true; + // Save Last ArcIndex + + } + + private Arc CreateTransition(string sWord, float flWeight, int requiredConfidence) + { + // epsilon transition for empty words + return AddSingleWordTransition(!string.IsNullOrEmpty(sWord) ? sWord : null, flWeight, requiredConfidence); + } + + private CfgGrammar.CfgSerializedHeader BuildHeader(List sortedStates, int cBasePath, ushort iSemanticGlobals, out int cArcs, out float[] pWeights) + { + cArcs = 1; // Start with offset one! (0 indicates dead state). + pWeights = null; + + int cSemanticTags = 0; + int cLargest = 0; + + foreach (State state in sortedStates) + { + // For new states SerializeId is INFINITE so we set it correctly here. + // For existing states we preserve the index from loading, + // unless new states have been added in. + state.SerializeId = cArcs; + + int thisState = state.NumArcs; + +#if DEBUG + if (thisState == 0 && state.InArcs.IsEmpty && state.Rule._cStates > 1) + { + XmlParser.ThrowSrgsException(SRID.StateWithNoArcs); + } +#endif + cArcs += thisState; + if (cLargest < thisState) + { + cLargest = thisState; + } + cSemanticTags += state.NumSemanticTags; + } + + CfgGrammar.CfgSerializedHeader header = new(); + uint ulOffset = (uint)Marshal.SizeOf(typeof(CfgGrammar.CfgSerializedHeader)); + + header.FormatId = CfgGrammar._SPGDF_ContextFree; + _guid = Guid.NewGuid(); + header.GrammarGUID = _guid; + header.LangID = (ushort)_langId; + header.pszSemanticInterpretationGlobals = iSemanticGlobals; + header.cArcsInLargestState = cLargest; + + header.cchWords = _words.StringSize(); + header.cWords = _words.Count; + + // For compat with SAPI 5.x add one to cWords if there's more than one word. + // The CFGEngine code assumes cWords includes the initial empty-string word. + // See PS 11491 and 61982. + if (header.cWords > 0) + { + header.cWords++; + } + + header.pszWords = ulOffset; + ulOffset += (uint)_words.SerializeSize() * Helpers._sizeOfChar; + header.cchSymbols = _symbols.StringSize(); + header.pszSymbols = ulOffset; + ulOffset += (uint)_symbols.SerializeSize() * Helpers._sizeOfChar; + header.cRules = _rules.Count; + header.pRules = ulOffset; + ulOffset += (uint)(_rules.Count * Marshal.SizeOf(typeof(CfgRule))); + header.cBasePath = cBasePath > 0 ? ulOffset : 0; //If there is no base path offset is set to zero + ulOffset += (uint)((cBasePath * Helpers._sizeOfChar + 3) & ~3); + header.cArcs = cArcs; + header.pArcs = ulOffset; + ulOffset += (uint)(cArcs * Marshal.SizeOf(typeof(CfgArc))); + if (_fNeedWeightTable) + { + header.pWeights = ulOffset; + ulOffset += (uint)(cArcs * Marshal.SizeOf(typeof(float))); + pWeights = new float[cArcs]; + pWeights[0] = 0.0f; + } + else + { + header.pWeights = 0; + ulOffset += 0; + } + + if (_rootRule != null) + { + //We have a root rule + header.ulRootRuleIndex = (uint)_rootRule._iSerialize; + } + else + { + //-1 means there is no root rule + header.ulRootRuleIndex = 0xFFFFFFFF; + } + + header.GrammarOptions = _grammarOptions | ((_alphabet == AlphabetType.Sapi) ? 0 : GrammarOptions.IpaPhoneme); + header.GrammarOptions |= _scriptRefs.Count > 0 ? GrammarOptions.STG | GrammarOptions.KeyValuePairSrgs : 0; + header.GrammarMode = (uint)_grammarMode; + header.cTags = cSemanticTags; + header.tags = ulOffset; + ulOffset += (uint)(cSemanticTags * Marshal.SizeOf(typeof(CfgSemanticTag))); + header.cScripts = _scriptRefs.Count; + header.pScripts = header.cScripts > 0 ? ulOffset : 0; + ulOffset += (uint)(_scriptRefs.Count * Marshal.SizeOf(typeof(CfgScriptRef))); + header.cIL = 0; + header.pIL = 0; + ulOffset += (uint)(header.cIL * Marshal.SizeOf(typeof(byte))); + header.cPDB = 0; + header.pPDB = 0; + ulOffset += (uint)(header.cPDB * Marshal.SizeOf(typeof(byte))); + header.ulTotalSerializedSize = ulOffset; + return header; + } + + private CfgGrammar.CfgHeader BuildRulesFromBinaryGrammar(CfgGrammar.CfgHeader header, State[] apStateTable, SortedDictionary ruleFirstArcs, int previousCfgLastRules) + { + for (int i = 0; i < header.rules.Length; i++) + { + // Check if the rule does not exist already + CfgRule cfgRule = header.rules[i]; + int firstArc = (int)cfgRule.FirstArcIndex; + + cfgRule._nameOffset = _symbols.OffsetFromId(_symbols.Find(header.pszSymbols.FromOffset(cfgRule._nameOffset))); + + Rule rule = new(this, _symbols.FromOffset(cfgRule._nameOffset), cfgRule, i + previousCfgLastRules, _grammarOptions & GrammarOptions.TagFormat, ref _cImportedRules); + + rule._firstState = _states.CreateNewState(rule); + _rules.Add(rule); + + // Add the rule to the list of firstArc/rule + if (firstArc > 0) + { + ruleFirstArcs.Add((int)cfgRule.FirstArcIndex, rule); + } + + rule._fStaticRule = (cfgRule.Dynamic) ? false : true; + rule._cfgRule.DirtyRule = false; + + // by default loaded static rules have an exist + rule._fHasExitPath = (rule._fStaticRule) ? true : false; + + // or they wouldn't be there in the first place + if (firstArc != 0) + { + System.Diagnostics.Debug.Assert(apStateTable[firstArc] == null); + rule._firstState.SerializeId = (int)cfgRule.FirstArcIndex; + apStateTable[firstArc] = rule._firstState; + } + + if (rule._cfgRule.HasResources) + { + throw new NotImplementedException(); + } + + if (header.ulRootRuleIndex == i) + { + _rootRule = rule; + } + + // Add rule to RuleListByName and RuleListByID hash tables. + if (rule._cfgRule._nameOffset != 0) + { + // Look for the rule in the original CFG and map it in the combined string blobs + _nameOffsetRules.Add(rule._cfgRule._nameOffset, rule); + } + } + return header; + } + + private Rule CloneState(State srcToState, List CloneStack, Dictionary srcToDestHash) + { + bool newRule = false; + int posDynamic = srcToState.Rule.Name.IndexOf("URL:DYNAMIC#", StringComparison.Ordinal); + string ruleName = posDynamic != 0 ? srcToState.Rule.Name : srcToState.Rule.Name.Substring(12); + Rule dstRule = FindInRules(ruleName); + + // Clone this rule into this GrammarBuilder if it does not exist yet + if (dstRule == null) + { + dstRule = srcToState.Rule.Clone(_symbols, ruleName); + _rules.Add(dstRule); + newRule = true; + } + + // Should not exist yet + System.Diagnostics.Debug.Assert(!srcToDestHash.ContainsKey(srcToState)); + + // push all the states for that rule + State newState = CreateNewState(dstRule); + srcToDestHash.Add(srcToState, newState); + CloneStack.Add(srcToState); + + if (newRule) + { + dstRule._firstState = newState; + } + + return dstRule; + } + + private Rule FindInRules(string ruleName) + { + foreach (Rule rule in _rules) + { + if (rule.Name == ruleName) + { + return rule; + } + } + return null; + } + + private static void LogError(string rule, SRID srid, params object[] args) + { + string sError = SR.Get(srid, args); + throw new FormatException(string.Format(CultureInfo.InvariantCulture, "Rule=\"{0}\" - ", rule) + sError); + } + + /// + /// Connect arc to the state graph. + /// +#if DEBUG + private +#else + private static +#endif + void AddArc(Arc arc) + { +#if DEBUG + arc.Backend = this; +#endif + } + + private void ValidateAndTagRules() + { + // + + bool fAtLeastOneRule = false; + int ulIndex = 0; + + foreach (Rule rule in _rules) + { + // set _fHasExitPath = true for empty dynamic grammars and imported rules + // Clear this for the next loop through the rules.... + rule._fHasExitPath |= (rule._cfgRule.Dynamic | rule._cfgRule.Import) ? true : false; + rule._iSerialize = ulIndex++; + fAtLeastOneRule |= (rule._cfgRule.Dynamic || rule._cfgRule.TopLevel || rule._cfgRule.Export); + rule.Validate(); + } +#if DEBUG + // + // Now make sure that all rules have an exit path. + // + foreach (Rule rule in _rules) + { + _ulRecursiveDepth = 0; + + //The following function will use recursive function that might change _ulRecursiveDepth + rule.CheckForExitPath(ref _ulRecursiveDepth); + } +#endif + // + // Check each exported rule if it has a dynamic rule in its "scope" + // + foreach (Rule rule in _rules) + { + if (rule._cfgRule.Dynamic) + { + rule._cfgRule.HasDynamicRef = true; + _ulRecursiveDepth = 0; + rule.PopulateDynamicRef(ref _ulRecursiveDepth); + } + } + } + + private void CheckLeftRecursion(List states) + { + bool fReachedEndState; + foreach (State state in states) + { + state.CheckLeftRecursion(out fReachedEndState); + } + } + + private Arc AddSingleWordTransition(string s, float flWeight, int requiredConfidence) + { + + Arc arc = new(s, null, _words, flWeight, requiredConfidence, null, MatchMode.AllWords, ref _fNeedWeightTable); + AddArc(arc); + return arc; + } + + internal void AddState(State state) + { + _states.Add(state); + } + + #endregion + + #region Internal Properties + + internal int LangId + { + get + { + return _langId; + } + set + { + _langId = value; + } + } + + internal GrammarOptions GrammarOptions + { + get + { + return _grammarOptions; + } + set + { + _grammarOptions = value; + } + } + + internal GrammarType GrammarMode + { + set + { + _grammarMode = value; + } + } + + internal AlphabetType Alphabet + { + get + { + return _alphabet; + } + set + { + _alphabet = value; + } + } + + internal Collection GlobalTags + { + get + { + return _globalTags; + } + set + { + _globalTags = value; + } + } + + internal Collection ScriptRefs + { + set + { + _scriptRefs = value; + } + } + + #endregion + + #region Private Fields + + private int _langId = CultureInfo.CurrentUICulture.LCID; + + private StringBlob _words; + + private StringBlob _symbols; + + //private int _cResources; + + private Guid _guid; + + private bool _fNeedWeightTable; + + private Graph _states = new(); + + private List _rules = new(); + + private int _ruleIndex; + + private Dictionary _nameOffsetRules = new(); + + private Rule _rootRule; + + private GrammarOptions _grammarOptions = GrammarOptions.KeyValuePairs; + + // It is used sequentially. So there is no thread issue + private int _ulRecursiveDepth; + + // Path from which relative grammar imports are calculated. As specified by xml:base + private string _basePath; + + // Collection of all SemanticTags in the grammar (sorted by StartArc) + private List _tags = new(); + + // Voice or DTMF + private GrammarType _grammarMode = GrammarType.VoiceGrammar; + + // Pron information is either IPA or SAPI + private AlphabetType _alphabet = AlphabetType.Sapi; + + // Global value for the semantic interpretation tags + private Collection _globalTags = new(); + + // + private static byte[] s_abZero3 = new byte[] { 0, 0, 0 }; + + private static char[] s_achZero = new char[] { '\0' }; + private int _cImportedRules; + + // List of cd /reference Rule->rule 'on'method-> .NET method + private Collection _scriptRefs = new(); + + private bool _fLoadedFromBinary; + + #endregion + } +} diff --git a/src/libraries/System.Speech/src/Internal/SrgsCompiler/CFGGrammar.cs b/src/libraries/System.Speech/src/Internal/SrgsCompiler/CFGGrammar.cs new file mode 100644 index 00000000000000..67bac9e6029aa9 --- /dev/null +++ b/src/libraries/System.Speech/src/Internal/SrgsCompiler/CFGGrammar.cs @@ -0,0 +1,574 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.IO; +using System.Runtime.InteropServices; +using System.Speech.Internal.SrgsParser; + +namespace System.Speech.Internal.SrgsCompiler +{ + internal sealed class CfgGrammar + { + #region Constructors + + internal CfgGrammar() + { + } + + #endregion + + #region Internal Types + + // Preprocess CFG header file + internal struct CfgHeader + { + internal Guid FormatId; + + internal Guid GrammarGUID; + + internal ushort langId; + + internal ushort pszGlobalTags; + + internal int cArcsInLargestState; + + internal StringBlob pszWords; + + internal StringBlob pszSymbols; + + internal CfgRule[] rules; + + internal CfgArc[] arcs; + + internal float[] weights; + + internal CfgSemanticTag[] tags; + + internal CfgScriptRef[] scripts; + + internal uint ulRootRuleIndex; + + internal GrammarOptions GrammarOptions; + + internal GrammarType GrammarMode; + + internal string BasePath; + } + + [StructLayout(LayoutKind.Sequential)] + internal class CfgSerializedHeader + { + internal CfgSerializedHeader() + { + } + +#pragma warning disable 56518 // The Binary reader cannot be disposed or it would close the underlying stream + + // Initializes a CfgSerializedHeader from a Stream. + // If the data does not represent a cfg then UnsuportedFormatException is thrown. + // This isn't a conclusive validity check, but is enough to determine if it's a CFG or not. + // For a complete check CheckValidCfgFormat is used. + internal CfgSerializedHeader(Stream stream) + { + BinaryReader br = new(stream); + ulTotalSerializedSize = br.ReadUInt32(); + if (ulTotalSerializedSize < SP_SPCFGSERIALIZEDHEADER_500 || ulTotalSerializedSize > int.MaxValue) + { + // Size is either negative or too small. + XmlParser.ThrowSrgsException(SRID.UnsupportedFormat); + } + + FormatId = new Guid(br.ReadBytes(16)); + if (FormatId != CfgGrammar._SPGDF_ContextFree) + { + // Not of cfg format + XmlParser.ThrowSrgsException(SRID.UnsupportedFormat); + } + + GrammarGUID = new Guid(br.ReadBytes(16)); + LangID = br.ReadUInt16(); + pszSemanticInterpretationGlobals = br.ReadUInt16(); + cArcsInLargestState = br.ReadInt32(); + cchWords = br.ReadInt32(); + cWords = br.ReadInt32(); + pszWords = br.ReadUInt32(); + if (pszWords < SP_SPCFGSERIALIZEDHEADER_500 || pszWords > ulTotalSerializedSize) + { + // First data points before or before valid range. + XmlParser.ThrowSrgsException(SRID.UnsupportedFormat); + } + + cchSymbols = br.ReadInt32(); + pszSymbols = br.ReadUInt32(); + cRules = br.ReadInt32(); + pRules = br.ReadUInt32(); + cArcs = br.ReadInt32(); + pArcs = br.ReadUInt32(); + pWeights = br.ReadUInt32(); + cTags = br.ReadInt32(); + tags = br.ReadUInt32(); + ulReservered1 = br.ReadUInt32(); + ulReservered2 = br.ReadUInt32(); + + if (pszWords > SP_SPCFGSERIALIZEDHEADER_500) + { + cScripts = br.ReadInt32(); + pScripts = br.ReadUInt32(); + cIL = br.ReadInt32(); + pIL = br.ReadUInt32(); + cPDB = br.ReadInt32(); + pPDB = br.ReadUInt32(); + ulRootRuleIndex = br.ReadUInt32(); + GrammarOptions = (GrammarOptions)br.ReadUInt32(); + cBasePath = br.ReadUInt32(); + GrammarMode = br.ReadUInt32(); + ulReservered3 = br.ReadUInt32(); + ulReservered4 = br.ReadUInt32(); + } + // Else SAPI 5.0 syntax grammar - parameters set to zero + } + internal static bool IsCfg(Stream stream, out int cfgLength) + { + cfgLength = 0; + BinaryReader br = new(stream); + uint ulTotalSerializedSize = br.ReadUInt32(); + if (ulTotalSerializedSize < SP_SPCFGSERIALIZEDHEADER_500 || ulTotalSerializedSize > int.MaxValue) + { + // Size is either negative or too small. + return false; + } + + Guid formatId = new(br.ReadBytes(16)); + if (formatId != CfgGrammar._SPGDF_ContextFree) + { + // Not of cfg format + return false; + } + + cfgLength = (int)ulTotalSerializedSize; + return true; + } + +#pragma warning restore 56518 // The Binary reader cannot be disposed or it would close the underlying stream + + internal uint ulTotalSerializedSize; + + internal Guid FormatId; + + internal Guid GrammarGUID; + + internal ushort LangID; + + internal ushort pszSemanticInterpretationGlobals; + + internal int cArcsInLargestState; + + internal int cchWords; + + internal int cWords; + + internal uint pszWords; + + internal int cchSymbols; + + internal uint pszSymbols; + + internal int cRules; + + internal uint pRules; + + internal int cArcs; + + internal uint pArcs; + + internal uint pWeights; + + internal int cTags; + + internal uint tags; + + internal uint ulReservered1; + + internal uint ulReservered2; + + internal int cScripts; + + internal uint pScripts; + + internal int cIL; + + internal uint pIL; + + internal int cPDB; + + internal uint pPDB; + + internal uint ulRootRuleIndex; + + internal GrammarOptions GrammarOptions; + + internal uint cBasePath; + + internal uint GrammarMode; + + internal uint ulReservered3; + + internal uint ulReservered4; + } + + internal class CfgProperty + { + internal string _pszName; + + internal uint _ulId; +#pragma warning disable 0618 // VarEnum is obsolete + internal VarEnum _comType; +#pragma warning restore 0618 + internal object _comValue; + } + + #endregion + + #region Internal Methods + + // + // This helper converts a serialized CFG grammar header into an in-memory header + // + internal static CfgHeader ConvertCfgHeader(StreamMarshaler streamHelper) + { + CfgSerializedHeader cfgSerializedHeader = null; + return ConvertCfgHeader(streamHelper, true, true, out cfgSerializedHeader); + } + + internal static CfgHeader ConvertCfgHeader(StreamMarshaler streamHelper, bool includeAllGrammarData, bool loadSymbols, out CfgSerializedHeader cfgSerializedHeader) + { + cfgSerializedHeader = new CfgSerializedHeader(streamHelper.Stream); + + // + // Because in 64-bit code, pointers != sizeof(ULONG) we copy each member explicitly. + // + + CfgHeader header = new(); + header.FormatId = cfgSerializedHeader.FormatId; + header.GrammarGUID = cfgSerializedHeader.GrammarGUID; + header.langId = cfgSerializedHeader.LangID; + header.pszGlobalTags = cfgSerializedHeader.pszSemanticInterpretationGlobals; + header.cArcsInLargestState = cfgSerializedHeader.cArcsInLargestState; + + // read all the common fields + header.rules = Load(streamHelper, cfgSerializedHeader.pRules, cfgSerializedHeader.cRules); + + if (includeAllGrammarData || loadSymbols) + { + header.pszSymbols = LoadStringBlob(streamHelper, cfgSerializedHeader.pszSymbols, cfgSerializedHeader.cchSymbols); + } + + if (includeAllGrammarData) + { + header.pszWords = LoadStringBlob(streamHelper, cfgSerializedHeader.pszWords, cfgSerializedHeader.cchWords); + header.arcs = Load(streamHelper, cfgSerializedHeader.pArcs, cfgSerializedHeader.cArcs); + header.tags = Load(streamHelper, cfgSerializedHeader.tags, cfgSerializedHeader.cTags); + header.weights = Load(streamHelper, cfgSerializedHeader.pWeights, cfgSerializedHeader.cArcs); + } + + //We know that in SAPI 5.0 grammar format pszWords follows header immediately. + if (cfgSerializedHeader.pszWords < Marshal.SizeOf(typeof(CfgSerializedHeader))) + { + //This is SAPI 5.0 and SAPI 5.1 grammar format + header.ulRootRuleIndex = 0xFFFFFFFF; + header.GrammarOptions = GrammarOptions.KeyValuePairs; + header.BasePath = null; + header.GrammarMode = GrammarType.VoiceGrammar; + } + else + { + //This is SAPI 5.2 and beyond grammar format + header.ulRootRuleIndex = cfgSerializedHeader.ulRootRuleIndex; + header.GrammarOptions = cfgSerializedHeader.GrammarOptions; + header.GrammarMode = (GrammarType)cfgSerializedHeader.GrammarMode; + if (includeAllGrammarData) + { + header.scripts = Load(streamHelper, cfgSerializedHeader.pScripts, cfgSerializedHeader.cScripts); + } + // The BasePath string is written after the rules - no offset is provided + // Get the chars and build the string + if (cfgSerializedHeader.cBasePath > 0) + { + streamHelper.Stream.Position = (int)cfgSerializedHeader.pRules + (header.rules.Length * Marshal.SizeOf(typeof(CfgRule))); + header.BasePath = streamHelper.ReadNullTerminatedString(); + } + } + + // Check the content - should be valid for both SAPI 5.0 and SAPI 5.2 grammars + CheckValidCfgFormat(cfgSerializedHeader, header, includeAllGrammarData); + + return header; + } + + // + // This helper converts a serialized CFG grammar header into an in-memory header + // + internal static ScriptRef[] LoadScriptRefs(StreamMarshaler streamHelper, CfgSerializedHeader pFH) + { + // + // Because in 64-bit code, pointers != sizeof(ULONG) we copy each member explicitly. + // + if (pFH.FormatId != CfgGrammar._SPGDF_ContextFree) + { + return null; + } + + //We know that in SAPI 5.0 grammar format pszWords follows header immediately. + if (pFH.pszWords < Marshal.SizeOf(typeof(CfgSerializedHeader))) + { + // Must be SAPI 6.0 or above to hold a .NET script + return null; + } + + // Get the symbols + StringBlob symbols = LoadStringBlob(streamHelper, pFH.pszSymbols, pFH.cchSymbols); + + // Get the script refs + CfgScriptRef[] cfgScripts = Load(streamHelper, pFH.pScripts, pFH.cScripts); + + // Convert the CFG script reference to ScriptRef + ScriptRef[] scripts = new ScriptRef[cfgScripts.Length]; + for (int i = 0; i < cfgScripts.Length; i++) + { + CfgScriptRef cfgScript = cfgScripts[i]; + scripts[i] = new ScriptRef(symbols[cfgScript._idRule], symbols[cfgScript._idMethod], cfgScript._method); + } + + return scripts; + } + + internal static ScriptRef[] LoadIL(Stream stream) + { + using (StreamMarshaler streamHelper = new(stream)) + { + CfgSerializedHeader pFH = new(); + + streamHelper.ReadStream(pFH); + + return LoadScriptRefs(streamHelper, pFH); + } + } + + internal static bool LoadIL(Stream stream, out byte[] assemblyContent, out byte[] assemblyDebugSymbols, out ScriptRef[] scripts) + { + assemblyContent = assemblyDebugSymbols = null; + scripts = null; + + using (StreamMarshaler streamHelper = new(stream)) + { + CfgSerializedHeader pFH = new(); + + streamHelper.ReadStream(pFH); + + scripts = LoadScriptRefs(streamHelper, pFH); + if (scripts == null) + { + return false; + } + + // Return if no script + if (pFH.cIL == 0) + { + return false; + } + + // Get the assembly content + assemblyContent = Load(streamHelper, pFH.pIL, pFH.cIL); + + assemblyDebugSymbols = pFH.cPDB > 0 ? Load(streamHelper, pFH.pPDB, pFH.cPDB) : null; + } + + return true; + } + + #endregion + + #region Private Methods + + private static void CheckValidCfgFormat(CfgSerializedHeader pFH, CfgHeader header, bool includeAllGrammarData) + { + //See backend commit method to understand the layout of cfg format + if (pFH.pszWords < SP_SPCFGSERIALIZEDHEADER_500) + { + XmlParser.ThrowSrgsException(SRID.UnsupportedFormat); + } + + int ullStartOffset = (int)pFH.pszWords; + + //Check the word offset + //See stringblob implementation. pFH.cchWords * sizeof(WCHAR) isn't exactly the serialized size, but it is close and must be less than the serialized size + CheckSetOffsets(pFH.pszWords, pFH.cchWords * Helpers._sizeOfChar, ref ullStartOffset, pFH.ulTotalSerializedSize); + + //Check the symbol offset + //symbol is right after word + //pFH.pszSymbols is very close to pFH.pszWords + pFH.cchWords * sizeof(WCHAR) + CheckSetOffsets(pFH.pszSymbols, pFH.cchSymbols * Helpers._sizeOfChar, ref ullStartOffset, pFH.ulTotalSerializedSize); + + //Check the rule offset + if (pFH.cRules > 0) + { + CheckSetOffsets(pFH.pRules, pFH.cRules * Marshal.SizeOf(typeof(CfgRule)), ref ullStartOffset, pFH.ulTotalSerializedSize); + } + + //Check the arc offset + if (pFH.cArcs > 0) + { + CheckSetOffsets(pFH.pArcs, pFH.cArcs * Marshal.SizeOf(typeof(CfgArc)), ref ullStartOffset, pFH.ulTotalSerializedSize); + } + + //Check the weight offset + if (pFH.pWeights > 0) + { + CheckSetOffsets(pFH.pWeights, pFH.cArcs * Marshal.SizeOf(typeof(float)), ref ullStartOffset, pFH.ulTotalSerializedSize); + } + + //Check the semantic tag offset + if (pFH.cTags > 0) + { + CheckSetOffsets(pFH.tags, pFH.cTags * Marshal.SizeOf(typeof(CfgSemanticTag)), ref ullStartOffset, pFH.ulTotalSerializedSize); + + if (includeAllGrammarData) + { + //Validate the SPCFGSEMANTICTAG array pointed to by tags + //We use header for easy array access + //The first arc is dummy, so the start and end arcindex for semantic tag won't be zero + for (int i = 0; i < header.tags.Length; i++) + { + int startArc = (int)header.tags[i].StartArcIndex; + int endArc = (int)header.tags[i].EndArcIndex; + int cArcs = header.arcs.Length; +#pragma warning disable 0618 // VarEnum is obsolete + if (startArc == 0 || + startArc >= cArcs || + endArc == 0 || + endArc >= cArcs || + ( + header.tags[i].PropVariantType != VarEnum.VT_EMPTY && + header.tags[i].PropVariantType != VarEnum.VT_BSTR && + header.tags[i].PropVariantType != VarEnum.VT_BOOL && + header.tags[i].PropVariantType != VarEnum.VT_R8 && + header.tags[i].PropVariantType != VarEnum.VT_I4) + ) + { + XmlParser.ThrowSrgsException(SRID.UnsupportedFormat); + } +#pragma warning restore 0618 + } + } + } + + //Check the offset for the scripts + if (pFH.cScripts > 0) + { + CheckSetOffsets(pFH.pScripts, pFH.cScripts * Marshal.SizeOf(typeof(CfgScriptRef)), ref ullStartOffset, pFH.ulTotalSerializedSize); + } + + if (pFH.cIL > 0) + { + CheckSetOffsets(pFH.pIL, pFH.cIL * Marshal.SizeOf(typeof(byte)), ref ullStartOffset, pFH.ulTotalSerializedSize); + } + + if (pFH.cPDB > 0) + { + CheckSetOffsets(pFH.pPDB, pFH.cPDB * Marshal.SizeOf(typeof(byte)), ref ullStartOffset, pFH.ulTotalSerializedSize); + } + } + + private static void CheckSetOffsets(uint offset, int size, ref int start, uint max) + { + if (offset < (uint)start || + (start = (int)offset + size) > (int)max) + { + XmlParser.ThrowSrgsException(SRID.UnsupportedFormat); + } + } + + private static StringBlob LoadStringBlob(StreamMarshaler streamHelper, uint iPos, int c) + { + char[] ach = new char[c]; + + streamHelper.Position = iPos; + streamHelper.ReadArrayChar(ach, c); + + return new StringBlob(ach); + } + + private static T[] Load(StreamMarshaler streamHelper, uint iPos, int c) + { + + T[] t = null; + + t = new T[c]; + + if (c > 0) + { + streamHelper.Position = iPos; + streamHelper.ReadArray(t, c); + } + + return t; + } + + #endregion + + #region Internal Properties + + internal static uint NextHandle + { + get + { + return ++s_lastHandle; + } + } + + #endregion + + #region Internal Fields + + internal static Guid _SPGDF_ContextFree = new(0x4ddc926d, 0x6ce7, 0x4dc0, 0x99, 0xa7, 0xaf, 0x9e, 0x6b, 0x6a, 0x4e, 0x91); + + // + internal const int INFINITE = unchecked((int)0xffffffff); + + // INFINITE + // + internal static readonly Rule SPRULETRANS_TEXTBUFFER = new(-1); + + internal static readonly Rule SPRULETRANS_WILDCARD = new(-2); + + internal static readonly Rule SPRULETRANS_DICTATION = new(-3); + + // + internal const int SPTEXTBUFFERTRANSITION = 0x3fffff; + + internal const int SPWILDCARDTRANSITION = 0x3ffffe; + + internal const int SPDICTATIONTRANSITION = 0x3ffffd; + + internal const int MAX_TRANSITIONS_COUNT = 256; + + internal const float DEFAULT_WEIGHT = 1f; + + // + internal const int SP_LOW_CONFIDENCE = -1; + + internal const int SP_NORMAL_CONFIDENCE = 0; + + internal const int SP_HIGH_CONFIDENCE = +1; + + #endregion + + #region Private Fields + + private const int SP_SPCFGSERIALIZEDHEADER_500 = 100; + + private static uint s_lastHandle; + + #endregion + } +} diff --git a/src/libraries/System.Speech/src/Internal/SrgsCompiler/CfgArc.cs b/src/libraries/System.Speech/src/Internal/SrgsCompiler/CfgArc.cs new file mode 100644 index 00000000000000..2f5aa214e31720 --- /dev/null +++ b/src/libraries/System.Speech/src/Internal/SrgsCompiler/CfgArc.cs @@ -0,0 +1,176 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Speech.Internal.SrgsParser; + +namespace System.Speech.Internal.SrgsCompiler +{ + internal struct CfgArc + { + #region Constructors + + internal CfgArc(CfgArc arc) + { + _flag1 = arc._flag1; + _flag2 = arc._flag2; + } + + #endregion + + #region Internal Properties + + internal bool RuleRef + { + get + { + return ((_flag1 & 0x1) != 0); + } + set + { + if (value) + { + _flag1 |= 0x1; + } + else + { + _flag1 &= ~0x1U; + } + } + } + + internal bool LastArc + { + get + { + return ((_flag1 & 0x2) != 0); + } + set + { + if (value) + { + _flag1 |= 0x2; + } + else + { + _flag1 &= ~0x2U; + } + } + } + + internal bool HasSemanticTag + { + get + { + return ((_flag1 & 0x4) != 0); + } + set + { + if (value) + { + _flag1 |= 0x4; + } + else + { + _flag1 &= ~0x4U; + } + } + } + + internal bool LowConfRequired + { + get + { + return ((_flag1 & 0x8) != 0); + } + set + { + if (value) + { + _flag1 |= 0x8; + } + else + { + _flag1 &= ~0x8U; + } + } + } + + internal bool HighConfRequired + { + get + { + return ((_flag1 & 0x10) != 0); + } + set + { + if (value) + { + _flag1 |= 0x10; + } + else + { + _flag1 &= ~0x10U; + } + } + } + + internal uint TransitionIndex + { + get + { + return (_flag1 >> 5) & 0x3FFFFF; + } + set + { + if (value > 0x3FFFFFU) + { + XmlParser.ThrowSrgsException(SRID.TooManyArcs); + } + + _flag1 &= ~(0x3FFFFFU << 5); + _flag1 |= value << 5; + } + } + + internal uint MatchMode + { + get + { + return (_flag1 >> 27) & 0x7; + } + set + { + _flag1 &= ~(0x38000000U); + _flag1 |= value << 27; + } + } + + internal uint NextStartArcIndex + { + get + { + return (_flag2 >> 8) & 0x3FFFFF; + } + set + { + if (value > 0x3FFFFF) + { + XmlParser.ThrowSrgsException(SRID.TooManyArcs); + } + + _flag2 &= ~(0x3FFFFFU << 8); + _flag2 |= value << 8; + } + } + + #endregion + + #region private Fields + + private uint _flag1; + + private uint _flag2; + + #endregion + } +} diff --git a/src/libraries/System.Speech/src/Internal/SrgsCompiler/CfgRule.cs b/src/libraries/System.Speech/src/Internal/SrgsCompiler/CfgRule.cs new file mode 100644 index 00000000000000..e7590dbf12433a --- /dev/null +++ b/src/libraries/System.Speech/src/Internal/SrgsCompiler/CfgRule.cs @@ -0,0 +1,233 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Speech.Internal.SrgsParser; + +namespace System.Speech.Internal.SrgsCompiler +{ + internal struct CfgRule + { + #region Constructors + + internal CfgRule(int id, int nameOffset, uint flag) + { + _flag = flag; + _nameOffset = nameOffset; + _id = id; + } + + internal CfgRule(int id, int nameOffset, SPCFGRULEATTRIBUTES attributes) + { + _flag = 0; + _nameOffset = nameOffset; + _id = id; + TopLevel = ((attributes & SPCFGRULEATTRIBUTES.SPRAF_TopLevel) != 0); + DefaultActive = ((attributes & SPCFGRULEATTRIBUTES.SPRAF_Active) != 0); + PropRule = ((attributes & SPCFGRULEATTRIBUTES.SPRAF_Interpreter) != 0); + Export = ((attributes & SPCFGRULEATTRIBUTES.SPRAF_Export) != 0); + Dynamic = ((attributes & SPCFGRULEATTRIBUTES.SPRAF_Dynamic) != 0); + Import = ((attributes & SPCFGRULEATTRIBUTES.SPRAF_Import) != 0); + } + + #endregion + + #region Internal Properties + + internal bool TopLevel + { + get + { + return ((_flag & 0x0001) != 0); + } + set + { + if (value) + { + _flag |= 0x0001; + } + else + { + _flag &= ~(uint)0x0001; + } + } + } + + internal bool DefaultActive + { + set + { + if (value) + { + _flag |= 0x0002; + } + else + { + _flag &= ~(uint)0x0002; + } + } + } + + internal bool PropRule + { + set + { + if (value) + { + _flag |= 0x0004; + } + else + { + _flag &= ~(uint)0x0004; + } + } + } + + internal bool Import + { + get + { + return ((_flag & 0x0008) != 0); + } + set + { + if (value) + { + _flag |= 0x0008; + } + else + { + _flag &= ~(uint)0x0008; + } + } + } + + internal bool Export + { + get + { + return ((_flag & 0x0010) != 0); + } + set + { + if (value) + { + _flag |= 0x0010; + } + else + { + _flag &= ~(uint)0x0010; + } + } + } + + internal bool HasResources + { + get + { + return ((_flag & 0x0020) != 0); + } + } + + internal bool Dynamic + { + get + { + return ((_flag & 0x0040) != 0); + } + set + { + if (value) + { + _flag |= 0x0040; + } + else + { + _flag &= ~(uint)0x0040; + } + } + } + + internal bool HasDynamicRef + { + get + { + return ((_flag & 0x0080) != 0); + } + set + { + if (value) + { + _flag |= 0x0080; + } + else + { + _flag &= ~(uint)0x0080; + } + } + } + + internal uint FirstArcIndex + { + get + { + return (_flag >> 8) & 0x3FFFFF; + } + set + { + if (value > 0x3FFFFF) + { + XmlParser.ThrowSrgsException(SRID.TooManyArcs); + } + + _flag &= ~((uint)0x3FFFFF << 8); + _flag |= value << 8; + } + } + + internal bool DirtyRule + { + set + { + if (value) + { + _flag |= 0x80000000; + } + else + { + _flag &= ~0x80000000; + } + } + } + + #endregion + + #region Internal Fields + + // should be private but the order is absolutely key for marshalling + internal uint _flag; + + internal int _nameOffset; + + internal int _id; + + #endregion + } + + #region Internal Enumeration + + [Flags] + internal enum SPCFGRULEATTRIBUTES + { + SPRAF_TopLevel = (1 << 0), + SPRAF_Active = (1 << 1), + SPRAF_Export = (1 << 2), + SPRAF_Import = (1 << 3), + SPRAF_Interpreter = (1 << 4), + SPRAF_Dynamic = (1 << 5), + SPRAF_Root = (1 << 6), + SPRAF_AutoPause = (1 << 16), + SPRAF_UserDelimited = (1 << 17) + } + + #endregion +} diff --git a/src/libraries/System.Speech/src/Internal/SrgsCompiler/CfgScriptRef.cs b/src/libraries/System.Speech/src/Internal/SrgsCompiler/CfgScriptRef.cs new file mode 100644 index 00000000000000..196023ea7bd2ec --- /dev/null +++ b/src/libraries/System.Speech/src/Internal/SrgsCompiler/CfgScriptRef.cs @@ -0,0 +1,23 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Runtime.InteropServices; +using System.Speech.Internal.SrgsParser; + +namespace System.Speech.Internal.SrgsCompiler +{ + [StructLayout(LayoutKind.Sequential)] + internal struct CfgScriptRef + { + #region Internal Fields + + // should be private but the order is absolutely key for marshalling + internal int _idRule; + + internal int _idMethod; + + internal RuleMethodScript _method; + + #endregion + } +} diff --git a/src/libraries/System.Speech/src/Internal/SrgsCompiler/CfgSemanticTag.cs b/src/libraries/System.Speech/src/Internal/SrgsCompiler/CfgSemanticTag.cs new file mode 100644 index 00000000000000..1d1568755f5ac0 --- /dev/null +++ b/src/libraries/System.Speech/src/Internal/SrgsCompiler/CfgSemanticTag.cs @@ -0,0 +1,207 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Runtime.InteropServices; +using System.Speech.Internal.SrgsParser; + +namespace System.Speech.Internal.SrgsCompiler +{ + [StructLayout(LayoutKind.Explicit)] + internal struct CfgSemanticTag + { + #region Constructors + + internal CfgSemanticTag(CfgSemanticTag cfgTag) + { + _flag1 = cfgTag._flag1; + _flag2 = cfgTag._flag2; + _flag3 = cfgTag._flag3; + _propId = cfgTag._propId; + _nameOffset = cfgTag._nameOffset; + _varInt = 0; + _valueOffset = cfgTag._valueOffset; + _varDouble = cfgTag._varDouble; + + // Initialize + StartArcIndex = 0x3FFFFF; + } + + internal CfgSemanticTag(StringBlob symbols, CfgGrammar.CfgProperty property) + { + int iWord; + + _flag1 = _flag2 = _flag3 = 0; + _valueOffset = 0; + _varInt = 0; + _varDouble = 0; + + _propId = property._ulId; + if (property._pszName != null) + { + _nameOffset = symbols.Add(property._pszName, out iWord); + } + else + { + _nameOffset = 0; // Offset must be zero if no string + } +#pragma warning disable 0618 // VarEnum is obsolete + switch (property._comType) + { + case 0: + case VarEnum.VT_BSTR: + if (property._comValue != null) + { + _valueOffset = symbols.Add((string)property._comValue, out iWord); + } + else + { + _valueOffset = 0; // Offset must be zero if no string + } + break; + + case VarEnum.VT_I4: + _varInt = (int)property._comValue; + break; + + case VarEnum.VT_BOOL: + _varInt = (bool)property._comValue ? unchecked(0xffff) : 0; + break; + + case VarEnum.VT_R8: + _varDouble = (double)property._comValue; + break; + + default: + System.Diagnostics.Debug.Assert(false, "Unknown Semantic Tag type"); + break; + } +#pragma warning restore 0618 + PropVariantType = property._comType; + ArcIndex = 0; + } + + #endregion + + #region Internal Properties + + internal uint StartArcIndex + { + get + { + return _flag1 & 0x3FFFFF; + } + set + { + if (value > 0x3FFFFF) + { + XmlParser.ThrowSrgsException(SRID.TooManyArcs); + } + + _flag1 &= ~(uint)0x3FFFFF; + _flag1 |= value; + } + } + + internal uint EndArcIndex + { + get + { + return _flag2 & 0x3FFFFF; + } + set + { + if (value > 0x3FFFFF) + { + XmlParser.ThrowSrgsException(SRID.TooManyArcs); + } + + _flag2 &= ~(uint)0x3FFFFF; + _flag2 |= value; + } + } + +#pragma warning disable 0618 // VarEnum is obsolete + internal VarEnum PropVariantType + { + get + { + return (VarEnum)(_flag3 & 0xFF); + } + set + { + uint varType = (uint)value; + + if (varType > 0xFF) + { + XmlParser.ThrowSrgsException(SRID.TooManyArcs); + } + + _flag3 &= ~(uint)0xFF; + _flag3 |= varType; + } + } +#pragma warning restore 0618 + + internal uint ArcIndex + { + get + { + return (_flag3 >> 8) & 0x3FFFFF; + } + set + { + if (value > 0x3FFFFF) + { + XmlParser.ThrowSrgsException(SRID.TooManyArcs); + } + + _flag3 &= ~((uint)0x3FFFFF << 8); + _flag3 |= value << 8; + } + } + + #endregion + + #region Internal Fields + + // Should be in the private section but the order for parameters is key + [FieldOffset(0)] + private uint _flag1; + + [FieldOffset(4)] + private uint _flag2; + + [FieldOffset(8)] + private uint _flag3; + + [FieldOffset(12)] + internal int _nameOffset; + + [FieldOffset(16)] + internal uint _propId; + + [FieldOffset(20)] + internal int _valueOffset; + [FieldOffset(24)] + internal int _varInt; + + [FieldOffset(24)] + internal double _varDouble; + + #endregion + } + + [Flags] + internal enum GrammarOptions + { + KeyValuePairs = 0, + MssV1 = 1, + KeyValuePairSrgs = 2, + IpaPhoneme = 4, + W3cV1 = 8, + STG = 0x10, + + TagFormat = KeyValuePairs | MssV1 | W3cV1 | KeyValuePairSrgs, + SemanticInterpretation = MssV1 | W3cV1 + }; +} diff --git a/src/libraries/System.Speech/src/Internal/SrgsCompiler/CustomGrammar.cs b/src/libraries/System.Speech/src/Internal/SrgsCompiler/CustomGrammar.cs new file mode 100644 index 00000000000000..bfdbd97035ff94 --- /dev/null +++ b/src/libraries/System.Speech/src/Internal/SrgsCompiler/CustomGrammar.cs @@ -0,0 +1,172 @@ +// 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.Globalization; +using System.IO; +using System.Reflection; +using System.Speech.Internal.SrgsParser; +using System.Text; + +#pragma warning disable 56507 // check for null or empty strings + +namespace System.Speech.Internal.SrgsCompiler +{ + internal class CustomGrammar + { + #region Constructors + + internal CustomGrammar() + { + } + + #endregion + + #region Internal methods + + /// + /// Add the scripts defined in 'cg' to the set of scripts defined in 'cgCombined'. + /// Build the union of t codebehind files and assembly references + /// + internal void Combine(CustomGrammar cg, string innerCode) + { + if (_rules.Count == 0) + { + _language = cg._language; + } + else + { + if (_language != cg._language) + { + XmlParser.ThrowSrgsException(SRID.IncompatibleLanguageProperties); + } + } + + if (_namespace == null) + { + _namespace = cg._namespace; + } + else + { + if (_namespace != cg._namespace) + { + XmlParser.ThrowSrgsException(SRID.IncompatibleNamespaceProperties); + } + } + + _fDebugScript |= cg._fDebugScript; + + foreach (string codebehind in cg._codebehind) + { + if (!_codebehind.Contains(codebehind)) + { + _codebehind.Add(codebehind); + } + } + + foreach (string assemblyReferences in cg._assemblyReferences) + { + if (!_assemblyReferences.Contains(assemblyReferences)) + { + _assemblyReferences.Add(assemblyReferences); + } + } + + foreach (string importNamespaces in cg._importNamespaces) + { + if (!_importNamespaces.Contains(importNamespaces)) + { + _importNamespaces.Add(importNamespaces); + } + } + + _keyFile = cg._keyFile; + + _types.AddRange(cg._types); + foreach (Rule rule in cg._rules) + { + if (_types.Contains(rule.Name)) + { + XmlParser.ThrowSrgsException(SRID.RuleDefinedMultipleTimes2, rule.Name); + } + } + + // Combine all the scripts + _script.Append(innerCode); + } + + #endregion + + #region Internal Properties + + internal bool HasScript + { + get + { + bool has_script = _script.Length > 0 || _codebehind.Count > 0; + if (!has_script) + { + foreach (Rule rule in _rules) + { + if (rule.Script.Length > 0) + { + has_script = true; + break; + } + } + } + return has_script; + } + } + + #endregion + + #region Internal Types + + internal class CfgResource + { + internal string name; + internal byte[] data; + } + + #endregion + + #region Internal Fields + + // 'C#', 'VB' or 'JScript' + internal string _language = "C#"; + + // namespace for the class wrapping the inline code + internal string _namespace; + + // namespace for the class wrapping the inline code + internal List _rules = new(); + + // code behind dll + internal Collection _codebehind = new(); + + // if set generates #line statements + internal bool _fDebugScript; + + // List of assembly references to import + internal Collection _assemblyReferences = new(); + + // List of namespaces to import + internal Collection _importNamespaces = new(); + + // Key file for the strong name + internal string _keyFile; + + // CFG scripts definition + internal Collection _scriptRefs = new(); + + // inline script + internal List _types = new(); + + // inline script + internal StringBuilder _script = new(); + + #endregion + } +} diff --git a/src/libraries/System.Speech/src/Internal/SrgsCompiler/GrammarElement.cs b/src/libraries/System.Speech/src/Internal/SrgsCompiler/GrammarElement.cs new file mode 100644 index 00000000000000..453087ca5c5a6f --- /dev/null +++ b/src/libraries/System.Speech/src/Internal/SrgsCompiler/GrammarElement.cs @@ -0,0 +1,370 @@ +// 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.Globalization; +using System.Speech.Internal.SrgsParser; + +namespace System.Speech.Internal.SrgsCompiler +{ + internal class GrammarElement : ParseElement, IGrammar + { + #region Constructors + + internal GrammarElement(Backend backend, CustomGrammar cg) + : base(null) + { + _cg = cg; + _backend = backend; + } + + #endregion + + #region Internal Method + + string IGrammar.Root + { + get + { + return _sRoot; + } + set + { + _sRoot = value; + } + } + + IRule IGrammar.CreateRule(string id, RulePublic publicRule, RuleDynamic dynamic, bool hasScript) + { + SPCFGRULEATTRIBUTES dwRuleAttributes = 0; + + // Determine rule attributes to apply based on RuleScope, IsDynamic, and IsRootRule. + // IsRootRule RuleScope IsDynamic Rule Attributes + // ---------------------------------------------------------------------- + // true * true Root | Active | TopLevel | Export | Dynamic + // true * false Root | Active | TopLevel | Export + // false internal true TopLevel | Export | Dynamic + // false internal false TopLevel | Export + // false private true Dynamic + // false private false 0 + if (id == _sRoot) + { + dwRuleAttributes |= SPCFGRULEATTRIBUTES.SPRAF_Root | SPCFGRULEATTRIBUTES.SPRAF_Active | SPCFGRULEATTRIBUTES.SPRAF_TopLevel; + _hasRoot = true; + } + + if (publicRule == RulePublic.True) + { + dwRuleAttributes |= SPCFGRULEATTRIBUTES.SPRAF_TopLevel | SPCFGRULEATTRIBUTES.SPRAF_Export; + } + + if (dynamic == RuleDynamic.True) + { + // BackEnd supports exported dynamic rules for SRGS grammars. + dwRuleAttributes |= SPCFGRULEATTRIBUTES.SPRAF_Dynamic; + } + + // Create rule with specified attributes + Rule rule = GetRule(id, dwRuleAttributes); + + // Add this rule to the list of rules of the STG list + if (publicRule == RulePublic.True || id == _sRoot || hasScript) + { + _cg._rules.Add(rule); + } + return rule; + } + + void IElement.PostParse(IElement parent) + { + if (_sRoot != null && !_hasRoot) + { + // "Root rule ""%s"" is undefined." + XmlParser.ThrowSrgsException(SRID.RootNotDefined, _sRoot); + } + + if (_undefRules.Count > 0) + { + // "Root rule ""%s"" is undefined." + Rule rule = _undefRules[0]; + XmlParser.ThrowSrgsException(SRID.UndefRuleRef, rule.Name); + } + + // SAPI semantics only for .NET Semantics + bool containsCode = ((IGrammar)this).CodeBehind.Count > 0 || ((IGrammar)this).ImportNamespaces.Count > 0 || ((IGrammar)this).AssemblyReferences.Count > 0 || CustomGrammar._scriptRefs.Count > 0; + if (containsCode && ((IGrammar)this).TagFormat != System.Speech.Recognition.SrgsGrammar.SrgsTagFormat.KeyValuePairs) + { + XmlParser.ThrowSrgsException(SRID.InvalidSemanticProcessingType); + } + } + + internal void AddScript(string name, string code) + { + foreach (Rule rule in _cg._rules) + { + if (rule.Name == name) + { + rule.Script.Append(code); + break; + } + } + } + + #endregion + + #region Internal Properties + + /// + /// Base URI of grammar (xml:base) + /// + Uri IGrammar.XmlBase + { + set + { + if (value != null) + { + _backend.SetBasePath(value.ToString()); + } + } + } + + /// + /// GrammarElement language (xml:lang) + /// + CultureInfo IGrammar.Culture + { + set + { + Helpers.ThrowIfNull(value, nameof(value)); + + _backend.LangId = value.LCID; + } + } + + /// + /// GrammarElement mode. voice or dtmf + /// + GrammarType IGrammar.Mode + { + set + { + _backend.GrammarMode = value; + } + } + + /// + /// GrammarElement mode. voice or dtmf + /// + AlphabetType IGrammar.PhoneticAlphabet + { + set + { + _backend.Alphabet = value; + } + } + + /// + /// Tag format (srgs:tag-format) + /// + System.Speech.Recognition.SrgsGrammar.SrgsTagFormat IGrammar.TagFormat + { + get + { + return System.Speech.Recognition.SrgsGrammar.SrgsDocument.GrammarOptions2TagFormat(_backend.GrammarOptions); + } + set + { + _backend.GrammarOptions = System.Speech.Recognition.SrgsGrammar.SrgsDocument.TagFormat2GrammarOptions(value); + } + } + + /// + /// Tag format (srgs:tag-format) + /// + Collection IGrammar.GlobalTags + { + get + { + return _backend.GlobalTags; + } + set + { + _backend.GlobalTags = value; + } + } + + internal List UndefRules + { + get + { + return _undefRules; + } + } + + internal Backend Backend + { + get + { + return _backend; + } + } + + /// + /// language + /// + string IGrammar.Language + { + get + { + return _cg._language; + } + set + { + _cg._language = value; + } + } + + /// + /// namespace + /// + string IGrammar.Namespace + { + get + { + return _cg._namespace; + } + set + { + _cg._namespace = value; + } + } + + /// + /// CodeBehind + /// + Collection IGrammar.CodeBehind + { + get + { + return _cg._codebehind; + } + set + { + _cg._codebehind = value; + } + } + + /// + /// Add #line statements to the inline scripts if set + /// + bool IGrammar.Debug + { + set + { + _cg._fDebugScript = value; + } + } + + /// + /// ImportNameSpaces + /// + Collection IGrammar.ImportNamespaces + { + get + { + return _cg._importNamespaces; + } + set + { + _cg._importNamespaces = value; + } + } + + /// + /// ImportNameSpaces + /// + Collection IGrammar.AssemblyReferences + { + get + { + return _cg._assemblyReferences; + } + set + { + _cg._assemblyReferences = value; + } + } + + internal CustomGrammar CustomGrammar + { + get + { + return _cg; + } + } + + #endregion + + #region Private Methods + + /// + /// Create a new rule with the specified name and attribute, and return the initial state. + /// Verify if Rule is unique. A Rule may already have been created as a placeholder during RuleRef. + /// + /// Rule name + /// Rule attributes + private Rule GetRule(string sRuleId, SPCFGRULEATTRIBUTES dwAttributes) + { + System.Diagnostics.Debug.Assert(!string.IsNullOrEmpty(sRuleId)); + + // Check if RuleID is unique. + Rule rule = _backend.FindRule(sRuleId); + + if (rule != null) + { + // Rule already defined. Check if it is a placeholder. + int iRule = _undefRules.IndexOf(rule); + + if (iRule != -1) + { + // This is a UndefinedRule created as a placeholder for a RuleRef. + // - Update placeholder rule with correct attributes. + _backend.SetRuleAttributes(rule, dwAttributes); + + // - Remove this now defined rule from UndefinedRules. + // Swap top element with this rule and pop the top element. + _undefRules.RemoveAt(iRule); + } + else + { + // Multiple definitions of the same Rule. + XmlParser.ThrowSrgsException(SRID.RuleRedefinition, sRuleId); // "Redefinition of rule ""%s""." + } + } + else + { + // Rule not yet defined. Create a new rule and return the InitalState. + rule = _backend.CreateRule(sRuleId, dwAttributes); + } + + return rule; + } + + #endregion + + #region Private Fields + + private Backend _backend; + + // Collection of referenced, but undefined, rules + private List _undefRules = new(); + private CustomGrammar _cg; + + private string _sRoot; + + private bool _hasRoot; + + #endregion + } +} diff --git a/src/libraries/System.Speech/src/Internal/SrgsCompiler/Graph.cs b/src/libraries/System.Speech/src/Internal/SrgsCompiler/Graph.cs new file mode 100644 index 00000000000000..01a4969288d672 --- /dev/null +++ b/src/libraries/System.Speech/src/Internal/SrgsCompiler/Graph.cs @@ -0,0 +1,995 @@ +// 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; +using System.Collections.Generic; +using System.Collections.ObjectModel; +using System.Diagnostics; +using System.Globalization; + +namespace System.Speech.Internal.SrgsCompiler +{ + // Doubled chained linked list for fast removal of states. + // Checks are made to ensure that the State pointers are never reused. + +#if DEBUG + [DebuggerDisplay("Count = {Count}")] + [DebuggerTypeProxy(typeof(GraphDebugDisplay))] +#endif + internal class Graph : IEnumerable + { + #region Internal Methods + + internal void Add(State state) + { + state.Init(); + if (_startState == null) + { + _curState = _startState = state; + } + else + { + _curState = _curState.Add(state); + } + } + + internal void Remove(State state) + { + if (state == _startState) + { + _startState = state.Next; + } + if (state == _curState) + { + _curState = state.Prev; + } + + state.Remove(); + } + + IEnumerator IEnumerable.GetEnumerator() + { + for (State item = _startState; item != null; item = item.Next) + { + yield return item; + } + } + + IEnumerator IEnumerable.GetEnumerator() + { + for (State item = _startState; item != null; item = item.Next) + { + yield return item; + } + } + + /// + /// Creates a new state handle in a given rule + /// + internal State CreateNewState(Rule rule) + { + uint hNewState = CfgGrammar.NextHandle; + + State newState = new(rule, hNewState); + Add(newState); +#if DEBUG + rule._cStates++; +#endif + return newState; + } + + /// + /// Delete a state + /// + internal void DeleteState(State state) + { +#if DEBUG + state.Rule._cStates--; +#endif + Remove(state); + } + + /// + /// Optimizes the grammar network by removing the epsilon states and merging + /// duplicate transitions. + /// + internal void Optimize() + { + foreach (State state in this) + { + NormalizeTransitionWeights(state); + } + +#if DEBUG + // Remove redundant epsilon transitions. + int cStates = Count; + RemoveEpsilonStates(); + if (Count != cStates) + { + System.Diagnostics.Trace.WriteLine("Grammar compiler, additional Epsilons could have been removed :" + (cStates - Count).ToString(CultureInfo.InvariantCulture)); + //System.Diagnostics.Debug.Assert (_states.Count == cStates); + } + // Remove duplicate transitions. +#endif + MergeDuplicateTransitions(); + +#if DEBUG + // Remove redundant epsilon transitions again now that identical epsilon transitions have been removed. + cStates = Count; + RemoveEpsilonStates(); + //System.Diagnostics.Debug.Assert (_states.Count == cStates); + if (Count != cStates) + { + System.Diagnostics.Trace.WriteLine("Grammar compiler, additional Epsilons could have been removed post merge transition :" + (cStates - Count).ToString(CultureInfo.InvariantCulture)); + } + + // Verify the transition weights are normalized. + foreach (State state in this) + { + double flSumWeights = 0.0f; // Compute the sum of the weights. + int cArcs = 0; + + foreach (Arc arc in state.OutArcs) + { + flSumWeights += arc.Weight; + cArcs++; + } + + float maxWeightError = 0.00001f * cArcs; + if (flSumWeights != 0.0f && maxWeightError - Math.Abs(flSumWeights - 1.0f) < 0) + { + System.Diagnostics.Debug.Assert(true); + } + } +#endif + } + + /// + /// Description: + /// Change all transitions ending at SourceState to end at DestState, instead. + /// Replace references to SourceState with references to DestState before deleting SourceState. + /// - There may be additional duplicate input transitions at DestState after the move. + /// + /// Assumptions: + /// - SourceState == !null, RuleInitialState, !DestState, ... + /// - DestState == null, RuleInitialState, !SourceState, ... + /// - SourceState.OutputArc.IsEmpty + /// - !(SourceState == RuleInitialState AND DestState == null) + /// + /// Algorithm: + /// - For each input transition into SourceState + /// - Transition.EndState = DestState + /// - If DestState != null, DestState.InputArcs += Transition + /// - SourceState.InputArcs -= Transition + /// - SourceState.InputArcs.Clear() + /// - If SourceState == RuleInitialState, RuleInitialState = DestState + /// - Delete SourceState + /// + internal void MoveInputTransitionsAndDeleteState(State srcState, State destState) + { + System.Diagnostics.Debug.Assert(srcState != null); + System.Diagnostics.Debug.Assert(srcState != destState); + + // For each input transition into SourceState, change EndState to DestState. + List arcs = srcState.InArcs.ToList(); + foreach (Arc arc in arcs) + { + // Change EndState to DestState + arc.End = destState; + } + + // Replace references to SourceState with references to DestState before deleting SourceState + if (srcState.Rule._firstState == srcState) // Update RuleInitialState reference, if necessary + { + System.Diagnostics.Debug.Assert(destState != null); + srcState.Rule._firstState = destState; + } + + // Delete SourceState + System.Diagnostics.Debug.Assert(srcState != null); + //System.Diagnostics.Debug.Assert (srcState.InArcs.IsEmpty); + System.Diagnostics.Debug.Assert(srcState.OutArcs.IsEmpty); + DeleteState(srcState); // Delete state from handle table + } + + /// + /// Description: + /// Change all transitions starting at SourceState to start at DestState, instead. + /// Deleting SourceState. + /// - The weights on the transitions have been properly adjusted. + /// The weights are not changed when moving transitions. + /// - There may be additional duplicate input transitions at DestState after the move. + /// + /// Assumptions: + /// - SourceState == !null, !RuleInitialState, !DestState, ... + /// - DestState == !null, RuleInitialState, !SourceState, ... + /// - SourceState.InputArc.IsEmpty + /// + /// Algorithm: + /// - For each output transition from SourceState + /// - Transition.StartState = DestState + /// - DestState.OutputArcs += Transition + /// - Delete SourceState + /// + internal void MoveOutputTransitionsAndDeleteState(State srcState, State destState) + { + System.Diagnostics.Debug.Assert(srcState != null); + System.Diagnostics.Debug.Assert((destState != null) && (destState != srcState)); + System.Diagnostics.Debug.Assert(srcState.InArcs.IsEmpty); + + // For each output transition from SourceState, change StartState to DestState. + List arcs = srcState.OutArcs.ToList(); + foreach (Arc arc in arcs) + { + // Change StartState to DestState + arc.Start = destState; + } + + // Delete SourceState + System.Diagnostics.Debug.Assert(srcState != null); + System.Diagnostics.Debug.Assert(srcState.InArcs.IsEmpty); + //System.Diagnostics.Debug.Assert (srcState.OutArcs.IsEmpty); + DeleteState(srcState); // Delete state from handle table + } + + #endregion + + #region Internal Property + +#if DEBUG + internal State First + { + get + { + return _startState; + } + } + + internal int Count + { + get + { + int c = 0; + for (State se = _startState; se != null; se = se.Next) + { + c++; + } + return c; + } + } + +#endif + #endregion + + #region Private Methods + +#if DEBUG + /// + /// Description: + /// Removing epsilon states from the grammar network. + /// - There may be additional duplicate transitions after removing epsilon transitions. + /// + /// Algorithm: + /// - For each State in the graph, + /// - If the state has a single input epsilon transition and is not the rule initial state, + /// - Move properties to the right, if necessary. + /// - If EpsilonTransition does not have properties and is not referenced by other properties, + /// - Delete EpsilonTransition. + /// - Multiply weight of all transitions from State by EpsilonTransition.Weight. + /// - MoveOutputTransitionsAndDeleteState(State, EpsilonTransition.StartState) + /// - If the state has a single output epsilon transition, + /// - Move properties to the left, if necessary. + /// - If EpsilonTransition does not have properties and is not referenced by other properties, + /// - Delete EpsilonTransition. + /// - MoveInputTransitionsAndDeleteState(State, EpsilonTransition.EndState) + /// + /// Moving SemanticTag: + /// - InputEpsilonTransitions can move its semantic tag ownerships/references to the right. + /// - OutputEpsilonTransitions can move its semantic tag ownerships/references to the left. + /// + private void RemoveEpsilonStates() + { + // For each state in the grammar graph, remove excess input/output epsilon transitions. + for (State state = First, nextState = null; state != null; state = nextState) + { + nextState = state.Next; + if (state.InArcs.CountIsOne && state.InArcs.First.IsEpsilonTransition && (state != state.Rule._firstState)) + { + // State has a single input epsilon transition and is not the rule initial state. + Arc epsilonArc = state.InArcs.First; + + // Attempt to move properties referencing EpsilonArc to the right. + // Optimization can only be applied when the epsilon arc is not referenced by any properties. + if (MoveSemanticTagRight(epsilonArc)) + { + // Delete the input epsilon transition + State pEpsilonStartState = epsilonArc.Start; + float flEpsilonWeight = epsilonArc.Weight; + + DeleteTransition(epsilonArc); + + // Multiply weight of all transitions from state by EpsilonWeight. + foreach (Arc arc in state.OutArcs) + { + arc.Weight *= flEpsilonWeight; + } + + // Move all output transitions from state to pEpsilonStartState and delete state if appropriate. + if (state != pEpsilonStartState) + { + MoveOutputTransitionsAndDeleteState(state, pEpsilonStartState); + } + } + } + // Optimize output epsilon transition, if possible + else if ((state.OutArcs.CountIsOne) && state.OutArcs.First.IsEpsilonTransition && (state != state.Rule._firstState)) + { + // State has a single output epsilon transition + Arc epsilonArc = state.OutArcs.First; + + // Attempt to move properties referencing EpsilonArc to the left. + // Optimization can only be applied when the epsilon arc is not referenced by any properties + // and when the arc does not connect RuleInitialState to null. + if (!((state == state.Rule._firstState) && (epsilonArc.End == null)) && MoveSemanticTagLeft(epsilonArc)) + { + // Delete the output epsilon transition + State pEpsilonEndState = epsilonArc.End; + + DeleteTransition(epsilonArc); + + // Move all input transitions from state to pEpsilonEndState and delete state if appropriate. + if (state != pEpsilonEndState) + { + MoveInputTransitionsAndDeleteState(state, pEpsilonEndState); + } + } + } + } + } +#endif + /// + /// Description: + /// Remove duplicate transitions starting from the same state, or ending at the same state. + /// + /// Algorithm: + /// - Add all states to ToDoList + /// - For each state left in the ToDoList, + /// - Merge any duplicate output transitions. + /// - Add all states to ToDoList in reverse order. + /// - Remove duplicate transitions to null (special case since there is no state for FinalState) + /// - For each state left in the ToDoList, + /// - Merge any duplicate input transitions. + /// + /// Notes: + /// - For best optimization, we need to move semantic properties referencing the transitions. + /// + private void MergeDuplicateTransitions() + { + List tempList = new(); + + // Build collection of states with potential identical transition. + foreach (State state in this) + { + if (state.OutArcs.ContainsMoreThanOneItem) + { + // Merge identical transitions in arcs + MergeIdenticalTransitions(state.OutArcs, tempList); + } + } + + // Collection of states with potential transitions to merge + Stack mergeStates = new(); + + RecursiveMergeDuplicatedOutputTransition(mergeStates); + RecursiveMergeDuplicatedInputTransition(mergeStates); + } + + private void RecursiveMergeDuplicatedInputTransition(Stack mergeStates) + { + // Build collection of states with potential duplicate input transitions to merge. + foreach (State state in this) + { + if (state.InArcs.ContainsMoreThanOneItem) + { + MergeDuplicateInputTransitions(state.InArcs, mergeStates); + } + } + + // For each state in the collection, merge any duplicate input transitions. + List tempList = new(); + while (mergeStates.Count > 0) + { + State state = mergeStates.Pop(); + if (state.InArcs.ContainsMoreThanOneItem) + { + // Merge identical transitions in arcs that may have been created + MergeIdenticalTransitions(state.InArcs, tempList); + MergeDuplicateInputTransitions(state.InArcs, mergeStates); + } + } + } + + private void RecursiveMergeDuplicatedOutputTransition(Stack mergeStates) + { + // Build collection of states with potential duplicate output transitions to merge. + foreach (State state in this) + { + if (state.OutArcs.ContainsMoreThanOneItem) + { + MergeDuplicateOutputTransitions(state.OutArcs, mergeStates); + } + } + + // For each state in the collection, merge any duplicate output transitions. + List tempList = new(); + while (mergeStates.Count > 0) + { + State state = mergeStates.Pop(); + if (state.OutArcs.ContainsMoreThanOneItem) + { + // Merge identical transitions in arcs that may have been created + MergeIdenticalTransitions(state.OutArcs, tempList); + MergeDuplicateOutputTransitions(state.OutArcs, mergeStates); + } + } + } + + /// + /// Description: + /// Sort and iterate through the input arcs and remove duplicate input transitions. + /// + /// Algorithm: + /// - MergeIdenticalTransitions(Arcs) + /// - Sort the input transitions from the state (by content and # output arcs from start state) + /// - For each set of transitions with identical content and StartState.OutputArcs.Count() == 1 + /// - Move semantic properties to the left, if necessary. + /// - Label the first property-less transition as CommonArc + /// - For each successive property-less transition (DuplicateArc) + /// - Delete DuplicateArc + /// - MoveInputTransitionsAndDeleteState(DuplicateArc.StartState, CommonArc.StartState) + /// - Add CommonArc.StartState to ToDoList if not there already. + /// + /// Moving SemanticTag: + /// - Duplicate input transitions can move its semantic tag ownerships/references to the left. + /// + /// Collection of input transitions to collapse + /// Collection of states with potential transitions to merge + private void MergeDuplicateInputTransitions(ArcList arcs, Stack mergeStates) + { + List arcsToMerge = null; + + // Reference Arc + Arc refArc = null; + bool refSet = false; + + // Build a list of possible arcs to Merge + foreach (Arc arc in arcs) + { + // Skip transitions whose end state has other incoming transitions or if the end state has more than one incoming transition + bool skipTransition = arc.Start == null || !arc.Start.OutArcs.CountIsOne; + // Find next set of duplicate output transitions (potentially with properties). + if (refArc != null && Arc.CompareContent(arc, refArc) == 0) + { + if (!skipTransition) + { + // Lazy init as entering this loop is a rare event + if (arcsToMerge == null) + { + arcsToMerge = new List(); + } + // Add the first element + if (!refSet) + { + arcsToMerge.Add(refArc); + refSet = true; + } + arcsToMerge.Add(arc); + } + } + else + { + // New word, reset everything + refArc = skipTransition ? null : arc; + refSet = false; + } + } + + // Combine the arcs if possible + if (arcsToMerge != null) + { + // Sort the arc per content and output transition + arcsToMerge.Sort(Arc.CompareForDuplicateInputTransitions); + + refArc = null; + Arc commonArc = null; // Common property-less transition to merge into + State commonStartState = null; + bool fCommonStartStateChanged = false; // Did CommonStartState change and need re-optimization? + + foreach (Arc arc in arcsToMerge) + { + if (refArc == null || Arc.CompareContent(arc, refArc) != 0) + { + // Purge the last operations and reset all the local + refArc = arc; + + // If CommonStartState changed, renormalize weights and add it to MergeStates for reoptimization. + if (fCommonStartStateChanged) + { + AddToMergeStateList(mergeStates, commonStartState); + } + + // Reset the arcs + commonArc = null; + commonStartState = null; + fCommonStartStateChanged = false; + } + + // For each property-less duplicate transition + Arc duplicatedArc = arc; + State duplicatedStartState = duplicatedArc.Start; + + // Attempt to move properties referencing duplicate arc to the right. + // Optimization can only be applied when the duplicate arc is not referenced by any properties + // and the duplicate end state is not the RuleOutitalState. + if (MoveSemanticTagLeft(duplicatedArc)) + { + // duplicatedArc != commonArc + if (commonArc != null) + { + if (!fCommonStartStateChanged) + { + // Processing first duplicate arc. + // Multiply the weights of transitions from CommonStartState by CommonArc.Weight. + foreach (Arc arcOut in commonStartState.OutArcs) + { + arcOut.Weight *= commonArc.Weight; + } + + fCommonStartStateChanged = true; // Output transitions of CommonStartState changed. + } + + // Multiply the weights of transitions from DuplicateStartState by DuplicateArc.Weight. + foreach (Arc arcOut in duplicatedStartState.OutArcs) + { + arcOut.Weight *= duplicatedArc.Weight; + } + + duplicatedArc.Weight += commonArc.Weight;// Merge duplicate arc weight with common arc + Arc.CopyTags(commonArc, duplicatedArc, Direction.Left); + DeleteTransition(commonArc); // Delete successive duplicate transitions + + // Move outputs of duplicate state to common state; Delete duplicate state + MoveInputTransitionsAndDeleteState(commonStartState, duplicatedStartState); + } + + // Label first property-less transition as CommonArc + commonArc = duplicatedArc; + commonStartState = duplicatedStartState; + } + } + // If CommonStartState changed, renormalize weights and add it to MergeStates for reoptimization. + if (fCommonStartStateChanged) + { + AddToMergeStateList(mergeStates, commonStartState); + } + } + } + + /// + /// Description: + /// Sort and iterate through the output arcs and remove duplicate output transitions. + /// + /// Algorithm: + /// - MergeIdenticalTransitions(Arcs) + /// - Sort the output transitions from the state (by content and # input arcs from end state) + /// - For each set of transitions with identical content, EndState != null, and EndState.InputArcs.Count() == 1 + /// - Move semantic properties to the right, if necessary. + /// - Label the first property-less transition as CommonArc + /// - For each property-less transition (DuplicateArc) including CommonArc + /// - Multiply the weights of output transitions from DuplicateArc.EndState by DuplicateArc.Weight. + /// - If DuplicateArc != CommonArc + /// - CommonArc.Weight += DuplicateArc.Weight + /// - Delete DuplicateArc + /// - MoveOutputTransitionsAndDeleteState(DuplicateArc.EndState, CommonArc.EndState) + /// - Normalize weights of output transitions from CommonArc.EndState. + /// - Add CommonArc.EndtState to ToDoList if not there already. + /// + /// Moving SemanticTag: + /// - Duplicate output transitions can move its semantic tag ownerships/references to the right. + /// + /// Collection of output transitions to collapse + /// Collection of states with potential transitions to merge + private void MergeDuplicateOutputTransitions(ArcList arcs, Stack mergeStates) + { + List arcsToMerge = null; + + // Reference Arc + Arc refArc = null; + bool refSet = false; + + // Build a list of possible arcs to Merge + foreach (Arc arc in arcs) + { + // Skip transitions whose end state has other incoming transitions or if the end state has more than one incoming transition + bool skipTransition = arc.End == null || !arc.End.InArcs.CountIsOne; + // Find next set of duplicate output transitions (potentially with properties). + if (refArc != null && Arc.CompareContent(arc, refArc) == 0) + { + if (!skipTransition) + { + // Lazy init as entering this loop is a rare event + if (arcsToMerge == null) + { + arcsToMerge = new List(); + } + // Add the first element + if (!refSet) + { + arcsToMerge.Add(refArc); + refSet = true; + } + arcsToMerge.Add(arc); + } + } + else + { + // New word, reset everything + refArc = skipTransition ? null : arc; + refSet = false; + } + } + + // Combine the arcs if possible + if (arcsToMerge != null) + { + // Sort the arc per content and output transition + arcsToMerge.Sort(Arc.CompareForDuplicateOutputTransitions); + + refArc = null; + Arc commonArc = null; // Common property-less transition to merge into + State commonEndState = null; + bool fCommonEndStateChanged = false; // Did CommonEndState change and need re-optimization? + + foreach (Arc arc in arcsToMerge) + { + if (refArc == null || Arc.CompareContent(arc, refArc) != 0) + { + // Purge the last operations and reset all the local + refArc = arc; + + // If CommonEndState changed, renormalize weights and add it to MergeStates for reoptimization. + if (fCommonEndStateChanged) + { + AddToMergeStateList(mergeStates, commonEndState); + } + + // Reset the arcs + commonArc = null; + commonEndState = null; + fCommonEndStateChanged = false; + } + + // For each property-less duplicate transition + Arc duplicatedArc = arc; + State duplicatedEndState = duplicatedArc.End; + + // Attempt to move properties referencing duplicate arc to the right. + // Optimization can only be applied when the duplicate arc is not referenced by any properties + // and the duplicate end state is not the RuleInitalState. + if ((duplicatedEndState != duplicatedEndState.Rule._firstState) && MoveSemanticTagRight(duplicatedArc)) + { + // duplicatedArc != commonArc + if (commonArc != null) + { + if (!fCommonEndStateChanged) + { + // Processing first duplicate arc. + // Multiply the weights of transitions from CommonEndState by CommonArc.Weight. + foreach (Arc arcOut in commonEndState.OutArcs) + { + arcOut.Weight *= commonArc.Weight; + } + + fCommonEndStateChanged = true; // Output transitions of CommonEndState changed. + } + + // Multiply the weights of transitions from DuplicateEndState by DuplicateArc.Weight. + foreach (Arc arcOut in duplicatedEndState.OutArcs) + { + arcOut.Weight *= duplicatedArc.Weight; + } + + duplicatedArc.Weight += commonArc.Weight;// Merge duplicate arc weight with common arc + Arc.CopyTags(commonArc, duplicatedArc, Direction.Right); + DeleteTransition(commonArc); // Delete successive duplicate transitions + + // Move outputs of duplicate state to common state; Delete duplicate state + MoveOutputTransitionsAndDeleteState(commonEndState, duplicatedEndState); + } + + // Label first property-less transition as CommonArc + commonArc = duplicatedArc; + commonEndState = duplicatedEndState; + } + } + // If CommonEndState changed, renormalize weights and add it to MergeStates for reoptimization. + if (fCommonEndStateChanged) + { + AddToMergeStateList(mergeStates, commonEndState); + } + } + } + + private static void AddToMergeStateList(Stack mergeStates, State commonEndState) + { + NormalizeTransitionWeights(commonEndState); + if (!mergeStates.Contains(commonEndState)) + { + mergeStates.Push(commonEndState); + } + } + + /// + /// Move any semantic tag ownership and optionally references to a unique + /// previous arc, if possible. + /// + /// MoveReferences = true: Return if arc is propertyless after the move. + /// MoveReferences = false: Return if arc does not own semantic tag after the move. + /// The arc can still be referenced by other semantic tags. + /// + internal static bool MoveSemanticTagLeft(Arc arc) + { + // This changes the range of words spanned by the tag, which is a bug for SAPI grammars. + State startState = arc.Start; + + // Can only move ownership/references if there is an unique input and output arc from the start state. + // Cannot concatenate semantic tags. (SemanticInterpretation script can arguably be concatenated.) + // Cannot move ownership across RuleRef (to maintain semantics of $$ in SemanticTag JScript). + // Cannot move semantic tag to special transition. (SREngine may return multiple result arcs for the transition.) + Arc previousArc = startState.InArcs.First; + if ((startState.InArcs.CountIsOne) && (startState.OutArcs.CountIsOne) && CanTagsBeMoved(previousArc, arc)) + { + // Move semantic tag ownership to the previous arc. + Arc.CopyTags(arc, previousArc, Direction.Left); + + // Semantic tag and optionally references have been moved successfully. + return true; + } + + return arc.IsPropertylessTransition; + } + + /// + /// Move any semantic tag ownership and optionally references to a unique + /// next arc, if possible. + /// + /// MoveReferences = true: Return if arc is propertyless after the move. + /// MoveReferences = false: Return if arc does not own semantic tag after the move. + /// The arc can still be referenced by other semantic tags. + /// + /// Force semantic tag references to always move with tag. + /// This changes the range of words spanned by the tag, which is a bug for SAPI grammars. + /// + internal static bool MoveSemanticTagRight(Arc arc) + { + System.Diagnostics.Debug.Assert(arc.End != null); + + State endState = arc.End; + + // Can only move ownership/references if there is an unique input and output arc from the end state. + // Cannot concatenate semantic tags. (SemanticInterpretation script can arguably be concatenated.) + // Cannot move ownership across RuleRef (to maintain semantics of $$ in SemanticTag JScript). + // Cannot move semantic tag to special transition. (SREngine may return multiple result arcs for the transition.) + Arc pNextArc = endState.OutArcs.First; + if ((endState.InArcs.CountIsOne) && (endState.OutArcs.CountIsOne) && CanTagsBeMoved(arc, pNextArc)) + { + // Move semantic tag ownership to the next arc. + Arc.CopyTags(arc, pNextArc, Direction.Right); + + // Semantic tag and optionally references have been moved successfully. + return true; + } + + return arc.IsPropertylessTransition; + } + + /// + /// Check if tags can be moved from a source arc to a destination + /// - Semantic interpretation. Tags cannot be moved if they would end up over a rule ref. + /// - Sapi properties. Tag can be put anywhere. + /// + internal static bool CanTagsBeMoved(Arc start, Arc end) + { + return (start.RuleRef == null) && (end.RuleRef == null) && (end.SpecialTransitionIndex == 0); + } + + /// + /// Description: + /// Detach and delete the specified transition from the graph. + /// Relocate or delete referencing semantic tags before deleting the transition. + /// + /// Special Case: + /// Arc.EndState == null + /// Arc.Optional == true + /// + private static void DeleteTransition(Arc arc) + { + // Arc cannot own SemanticTag + System.Diagnostics.Debug.Assert(arc.SemanticTagCount == 0); + + // Arc cannot be referenced by SemanticTags + System.Diagnostics.Debug.Assert(arc.IsPropertylessTransition); + + // Detach arc from start and end state + arc.Start = arc.End = null; + } + + /// + /// Description: + /// Merge identical transitions with identical content, StartState, and EndState. + /// + /// + private static void MergeIdenticalTransitions(ArcList arcs, List identicalWords) + { + // Need at least two transitions to merge. + System.Diagnostics.Debug.Assert(arcs.ContainsMoreThanOneItem); + + // Need at least two transitions to merge. + List> segmentsToDelete = null; + Arc refArc = arcs.First; + + // Accumulate a set of transition to delete + foreach (Arc arc in arcs) + { + if (Arc.CompareContent(refArc, arc) != 0) + { + // Identical transition + if (identicalWords.Count >= 2) + { + identicalWords.Sort(Arc.CompareIdenticalTransitions); + if (segmentsToDelete == null) + { + segmentsToDelete = new List>(); + } + + // Add the list of same words into a list for further processing. + // The expectation of having an identical transition is very low so the code + // may be a bit slow. + segmentsToDelete.Add(new List(identicalWords)); + } + identicalWords.Clear(); + } + refArc = arc; + identicalWords.Add(arc); + } + + // Did the last word was replicated several times? + if (identicalWords.Count >= 2) + { + MergeIdenticalTransitions(identicalWords); + } + identicalWords.Clear(); + + // Process the accumulated words + if (segmentsToDelete != null) + { + foreach (List segmentToDelete in segmentsToDelete) + { + MergeIdenticalTransitions(segmentToDelete); + } + } + } + + /// + /// Description: + /// Merge identical transitions with identical content, StartState, and EndState. + /// + /// Algorithm: + /// - LastArc = Arcs[0] + /// - For each Arc in Arcs[1-], + /// - If Arc is identical to LastArc, + /// - LastArc.Weight += Arc.Weight + /// - Delete Arc + /// - Else LastArc = Arc + /// + /// Moving SemanticTag: + /// - Identical transitions have identical semantic tags. Currently impossible to have identical + /// non-null tags. + /// - MoveSemanticTagReferences(DuplicateArc, CommonArc) + /// + private static void MergeIdenticalTransitions(List identicalWords) + { + Collection arcsToDelete = null; + Arc refArc = null; + foreach (Arc arc in identicalWords) + { + if (refArc != null && Arc.CompareIdenticalTransitions(refArc, arc) == 0) + { + // Identical transition + arc.Weight += refArc.Weight; + refArc.ClearTags(); + if (arcsToDelete == null) + { + // delay the creation of the collection as this operation in infrequent. + arcsToDelete = new Collection(); + } + arcsToDelete.Add(refArc); + } + refArc = arc; + } + if (arcsToDelete != null) + { + foreach (Arc arc in arcsToDelete) + { + // arc will become an orphan + DeleteTransition(arc); + } + } + } + + /// + /// Normalize the weights of output transitions from this state. + /// + private static void NormalizeTransitionWeights(State state) + { + float flSumWeights = 0.0f; + + // Compute the sum of the weights. + foreach (Arc arc in state.OutArcs) + { + flSumWeights += arc.Weight; + } + + // If Sum != 0 or 1, normalize transition weights by 1/Sum. + if (!flSumWeights.Equals(0.0f) && !flSumWeights.Equals(1.0f)) + { + float flNormalizationFactor = 1.0f / flSumWeights; + + foreach (Arc arc in state.OutArcs) + { + arc.Weight *= flNormalizationFactor; + } + } + } + + #endregion + + #region Private Types + +#if DEBUG + // Used by the debugger display attribute + internal class GraphDebugDisplay + { + public GraphDebugDisplay(Graph states) + { + _states = states; + } + [DebuggerBrowsable(DebuggerBrowsableState.RootHidden)] + public State[] AKeys + { + get + { + State[] states = new State[_states.Count]; + int i = 0; + foreach (State state in _states) + { + states[i++] = state; + } + return states; + } + } + + private Graph _states; + } +#endif + + #endregion + + #region Private Fields + + private State _startState; + private State _curState; + + #endregion + } +} diff --git a/src/libraries/System.Speech/src/Internal/SrgsCompiler/Item.cs b/src/libraries/System.Speech/src/Internal/SrgsCompiler/Item.cs new file mode 100644 index 00000000000000..5a92bd1c915f2f --- /dev/null +++ b/src/libraries/System.Speech/src/Internal/SrgsCompiler/Item.cs @@ -0,0 +1,161 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Speech.Internal.SrgsParser; + +namespace System.Speech.Internal.SrgsCompiler +{ + internal sealed class Item : ParseElementCollection, IItem + { + #region Constructors + + internal Item(Backend backend, Rule rule, int minRepeat, int maxRepeat, float repeatProbability, float weigth) + : base(backend, rule) + { + // Validated by the caller + _minRepeat = minRepeat; + _maxRepeat = maxRepeat; + _repeatProbability = repeatProbability; + } + + #endregion + + #region Internal Method + + /// + /// Process the '/item' element. + /// + void IElement.PostParse(IElement parentElement) + { + // Special case of no words but only tags. Returns an error as the result is ambiguous + // var res= 1; + // + // res= res * 2; + // + // Should the result be 2 or 4 + if (_maxRepeat != _minRepeat && _startArc != null && _startArc == _endArc && _endArc.IsEpsilonTransition && !_endArc.IsPropertylessTransition) + { + XmlParser.ThrowSrgsException((SRID.InvalidTagInAnEmptyItem)); + } + + // empty or repeat count == 0 + if (_startArc == null || _maxRepeat == 0) + { + // Special Case: _maxRepeat = 0 => Epsilon transition. + if (_maxRepeat == 0 && _startArc != null && _startArc.End != null) + { + // Delete contents of Item. Otherwise, we will end up with states disconnected to the rest of the rule. + State endState = _startArc.End; + _startArc.End = null; + _backend.DeleteSubGraph(endState); + } + // empty item, just add an epsilon transition. + _startArc = _endArc = _backend.EpsilonTransition(_repeatProbability); + } + else + { + // Hard case if repeat count is not one + if (_minRepeat != 1 || _maxRepeat != 1) + { + // Duplicate the states/transitions graph as many times as repeat count + + //Add a state before the start to be able to duplicate the graph + _startArc = InsertState(_startArc, _repeatProbability, Position.Before); + State startState = _startArc.End; + + // If _maxRepeat = Infinite, add epsilon transition loop back to the start of this + if (_maxRepeat == int.MaxValue && _minRepeat == 1) + { + _endArc = InsertState(_endArc, 1.0f, Position.After); + + AddEpsilonTransition(_endArc.Start, startState, 1 - _repeatProbability); + } + else + { + State currentStartState = startState; + + // For each additional repeat count, clone a new subgraph and connect with appropriate transitions. + for (uint cnt = 1; cnt < _maxRepeat && cnt < 255; cnt++) + { + // Prepare to clone a new subgraph matching the content. + State newStartState = _backend.CreateNewState(_endArc.Start.Rule); + + // Clone subgraphs and update CurrentEndState. + State newEndState = _backend.CloneSubGraph(currentStartState, _endArc.Start, newStartState); + + // Connect the last state with the first state + //_endArc.Start.OutArcs.Add (_endArc); + _endArc.End = newStartState; + + // reset the _endArc + System.Diagnostics.Debug.Assert(newEndState.OutArcs.CountIsOne && Arc.CompareContent(_endArc, newEndState.OutArcs.First) == 0); + _endArc = newEndState.OutArcs.First; + + if (_maxRepeat == int.MaxValue) + { + // If we are beyond _minRepeat, add epsilon transition from startState with (1-_repeatProbability). + if (cnt == _minRepeat - 1) + { + // Create a new state and attach the last Arc to add + _endArc = InsertState(_endArc, 1.0f, Position.After); + + AddEpsilonTransition(_endArc.Start, newStartState, 1 - _repeatProbability); + break; + } + } + else if (cnt <= _maxRepeat - _minRepeat) + { + // If we are beyond _minRepeat, add epsilon transition from startState with (1-_repeatProbability). + AddEpsilonTransition(startState, newStartState, 1 - _repeatProbability); + } + + // reset the current start state + currentStartState = newStartState; + } + } + // If _minRepeat == 0, add epsilon transition from currentEndState to FinalState with (1-_repeatProbability). + // but do not do it if the only transition is an epsilon + if (_minRepeat == 0 && (_startArc != _endArc || !_startArc.IsEpsilonTransition)) + { + if (!_endArc.IsEpsilonTransition || _endArc.SemanticTagCount > 0) + { + _endArc = InsertState(_endArc, 1.0f, Position.After); + } + AddEpsilonTransition(startState, _endArc.Start, 1 - _repeatProbability); + } + + // Remove the added startState if possible + _startArc = TrimStart(_startArc, _backend); + } + } + + // Add this item to the parent list + base.PostParse((ParseElementCollection)parentElement); + } + + #endregion + + #region Private Methods + + private void AddEpsilonTransition(State start, State end, float weight) + { + Arc epsilon = _backend.EpsilonTransition(weight); + epsilon.Start = start; + epsilon.End = end; + } + + #endregion + + #region Private Fields + + private float _repeatProbability = 0.5f; + + private int _minRepeat = NotSet; + + private int _maxRepeat = NotSet; + + private const int NotSet = -1; + + #endregion + } +} diff --git a/src/libraries/System.Speech/src/Internal/SrgsCompiler/OneOf.cs b/src/libraries/System.Speech/src/Internal/SrgsCompiler/OneOf.cs new file mode 100644 index 00000000000000..2ec811eb81ac41 --- /dev/null +++ b/src/libraries/System.Speech/src/Internal/SrgsCompiler/OneOf.cs @@ -0,0 +1,109 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +#region Using directives + +using System.Speech.Internal.SrgsParser; + +#endregion + +namespace System.Speech.Internal.SrgsCompiler +{ + internal class OneOf : ParseElementCollection, IOneOf + { + #region Constructors + + /// + /// Process the 'one-of' element. + /// + public OneOf(Rule rule, Backend backend) + : base(backend, rule) + { + // Create a start and end start. + _startState = _backend.CreateNewState(rule); + _endState = _backend.CreateNewState(rule); + + //Add before the start state an epsilon arc + _startArc = _backend.EpsilonTransition(1.0f); + _startArc.End = _startState; + + //Add after the end state an epsilon arc + _endArc = _backend.EpsilonTransition(1.0f); + _endArc.Start = _endState; + } + + #endregion + + #region Internal Method + + /// + /// Process the '/one-of' element. + /// Connects all the arcs into an exit end point. + /// + /// Verify OneOf contains at least one child 'item'. + /// + void IElement.PostParse(IElement parentElement) + { + if (_startArc.End.OutArcs.IsEmpty) + { + XmlParser.ThrowSrgsException(SRID.EmptyOneOf); + } + + // Remove the extraneous arc and state if possible at the start and end + _startArc = TrimStart(_startArc, _backend); + _endArc = TrimEnd(_endArc, _backend); + + // Connect the one-of to the parent + base.PostParse((ParseElementCollection)parentElement); + } + + #endregion + + #region Protected Method + + /// + /// Adds a new arc to the one-of + /// + internal override void AddArc(Arc start, Arc end) + { + start = TrimStart(start, _backend); + end = TrimEnd(end, _backend); + + State endStartState = end.Start; + State startEndState = start.End; + + // Connect the previous arc with the 'start' set the insertion point + if (start.IsEpsilonTransition & start.IsPropertylessTransition && startEndState != null && startEndState.InArcs.IsEmpty) + { + System.Diagnostics.Debug.Assert(start.End == startEndState); + start.End = null; + _backend.MoveOutputTransitionsAndDeleteState(startEndState, _startState); + } + else + { + start.Start = _startState; + } + + // Connect with the epsilon transition at the end + if (end.IsEpsilonTransition & end.IsPropertylessTransition && endStartState != null && endStartState.OutArcs.IsEmpty) + { + System.Diagnostics.Debug.Assert(end.Start == endStartState); + end.Start = null; + _backend.MoveInputTransitionsAndDeleteState(endStartState, _endState); + } + else + { + end.End = _endState; + } + } + + #endregion + + #region Protected Method + + private State _startState; + private State _endState; + + #endregion + } +} diff --git a/src/libraries/System.Speech/src/Internal/SrgsCompiler/ParseElement.cs b/src/libraries/System.Speech/src/Internal/SrgsCompiler/ParseElement.cs new file mode 100644 index 00000000000000..602c49fdbab077 --- /dev/null +++ b/src/libraries/System.Speech/src/Internal/SrgsCompiler/ParseElement.cs @@ -0,0 +1,52 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +#region Using directives + +using System.Speech.Internal.SrgsParser; + +#endregion + +namespace System.Speech.Internal.SrgsCompiler +{ + // Elements of the ParseStack + // SRGSNamespace.Grammar + // _startState, _endState are ignored and set to 0. + // SRGSNamespace.Rule + // startElement() _startState = new Rule().InitialState + // _endState = _startState (Updated by the child elements) + // endElement() AddEpsilonTransition(_endState -> terminating state null) + // SRGSNamespace.RuleRef/Token/Tag/Item(Parent!=OneOf) + // startElement() _startState = Parent._startState + // _endState = _startState (Updated by the child elements) + // endElement() Parent._endState = _endState + // SRGSNamespace.OneOf + // startElement() _startState = Parent._startState + // _endState = new State + // endElement() Parent._endState = _endState + // SRGSNamespace.Item(Parent==OneOf) + // startElement() _startState = Parent._startState + // _endState = _startState (Updated by the child elements) + // endElement() AddEpsilonTransition(_endState -> Parent._endState) + // SRGSNamespace.Example/Lexicon/Meta + // _startState, _endState are ignored and set to 0. + // SRGSNamespace.Metadata / Unknown.* + // _startState, _endState are ignored and set to 0. + // ParseElements is added to the stack, but not used. + internal abstract class ParseElement : IElement // Compiler stack element + { + internal ParseElement(Rule rule) + { + _rule = rule; + } + + // Token - Required confidence + internal int _confidence; + + void IElement.PostParse(IElement parent) + { + } + + internal Rule _rule; + } +} diff --git a/src/libraries/System.Speech/src/Internal/SrgsCompiler/ParseElementCollection.cs b/src/libraries/System.Speech/src/Internal/SrgsCompiler/ParseElementCollection.cs new file mode 100644 index 00000000000000..cd15cffe0b3e33 --- /dev/null +++ b/src/libraries/System.Speech/src/Internal/SrgsCompiler/ParseElementCollection.cs @@ -0,0 +1,307 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +#region Using directives + +#endregion + +namespace System.Speech.Internal.SrgsCompiler +{ + internal abstract class ParseElementCollection : ParseElement + { + protected ParseElementCollection(Backend backend, Rule rule) + : base(rule) + { + _backend = backend; + } + + /// + /// Attach a semantic tag to word. If the word is a rule ref then an + /// epsilon transition must be created + /// + internal void AddSemanticInterpretationTag(CfgGrammar.CfgProperty propertyInfo) + { + // If the word is a rule ref, an epsilon transition must be created + if (_endArc != null && _endArc.RuleRef != null) + { + Arc tagTransition = _backend.EpsilonTransition(1.0f); + _backend.AddSemanticInterpretationTag(tagTransition, propertyInfo); + + // Create a new state + State state = _backend.CreateNewState(_rule); + + // Connect the new state with the end arc + tagTransition.Start = state; + _endArc.End = state; + _endArc = tagTransition; + } + else + { + if (_startArc == null) + { + _startArc = _endArc = _backend.EpsilonTransition(1.0f); + } + _backend.AddSemanticInterpretationTag(_endArc, propertyInfo); + } + } + + // must add the rule Id + // _propInfo._ulId = (uint) ((ParseElement) parent).StartState._rule._iSerialize2; + internal void AddSementicPropertyTag(CfgGrammar.CfgProperty propertyInfo) + { + if (_startArc == null) + { + _startArc = _endArc = _backend.EpsilonTransition(1.0f); + } + _backend.AddPropertyTag(_startArc, _endArc, propertyInfo); + } + + /// + /// Insert an epsilon state either before or after the current arc + /// + protected Arc InsertState(Arc arc, float weight, Position position) + { + // If the arc is a epsilon, creating a new epsilon arc might not be needed + if (arc.IsEpsilonTransition) + { + if (position == Position.Before && arc.End != null && arc.End.InArcs.CountIsOne && Graph.MoveSemanticTagRight(arc)) + { + return arc; + } + if (position == Position.After && arc.Start != null && arc.Start.OutArcs.CountIsOne && Graph.MoveSemanticTagLeft(arc)) + { + return arc; + } + } + + // Create an epsilon transition + Arc epsilon = _backend.EpsilonTransition(weight); + + // Insert a state + State insertionState = _backend.CreateNewState(_rule); + + if (position == Position.Before) + { + epsilon.End = insertionState; + arc.Start = insertionState; + } + else + { + arc.End = insertionState; + epsilon.Start = insertionState; + } + return epsilon; + } + + /// + /// Remove all the epsilon transitions at the beginning of a sub graph + /// + protected static Arc TrimStart(Arc start, Backend backend) + { + Arc startArc = start; + + if (start.End != null) + { + // Remove the added startState if possible, check done by MoveSemanticTagRight + for (State startState = startArc.End; startArc.IsEpsilonTransition && startState != null && Graph.MoveSemanticTagRight(startArc) && startState.InArcs.CountIsOne && startState.OutArcs.CountIsOne; startState = startArc.End) + { + // State has a single input epsilon transition + // Delete the input epsilon transition and delete state. + System.Diagnostics.Debug.Assert(startArc.End == startState); + startArc.End = null; + + // Reset the start Arc + System.Diagnostics.Debug.Assert(startState.OutArcs.CountIsOne); + startArc = startState.OutArcs.First; + System.Diagnostics.Debug.Assert(startArc.Start == startState); + startArc.Start = null; + + // Delete the input epsilon transition and delete state if appropriate. + backend.DeleteState(startState); + } + } + return startArc; + } + + /// + /// Remove all the epsilon transition at the end + /// + protected static Arc TrimEnd(Arc end, Backend backend) + { + Arc endArc = end; + + if (endArc != null) + { + // Remove the end arc if possible, check done by MoveSemanticTagRight + for (State endState = endArc.Start; endArc.IsEpsilonTransition && endState != null && Graph.MoveSemanticTagLeft(endArc) && endState.InArcs.CountIsOne && endState.OutArcs.CountIsOne; endState = endArc.Start) + { + // State has a single input epsilon transition + // Delete the input epsilon transition and delete state. + System.Diagnostics.Debug.Assert(endArc.Start == endState); + endArc.Start = null; + + // Reset the end Arc + System.Diagnostics.Debug.Assert(endState.InArcs.CountIsOne); + endArc = endState.InArcs.First; + System.Diagnostics.Debug.Assert(endArc.End == endState); + endArc.End = null; + + // Delete the input epsilon transition and delete state if appropriate. + backend.DeleteState(endState); + } + } + return endArc; + } + + protected void PostParse(ParseElementCollection parent) + { + if (_startArc != null) + { + parent.AddArc(_startArc, _endArc); + } + } + + internal void AddArc(Arc arc) { AddArc(arc, arc); } + + internal enum Position + { + Before, + After + } + + /// + /// New sets of arcs are added after the last arc + /// + internal virtual void AddArc(Arc start, Arc end) + { + State state = null; + if (_startArc == null) + { + _startArc = start; + _endArc = end; + } + else + { + bool done = false; + + // Successive have 2 epsilon transition + if (_endArc.IsEpsilonTransition && start.IsEpsilonTransition) + { + // Trim the start tag. + start = TrimStart(start, _backend); + + // If Trimming didn't create a non epsilon, try to trim the end + if (start.IsEpsilonTransition) + { + _endArc = TrimEnd(_endArc, _backend); + + // start and end are still epsilon transition + if (_endArc.IsEpsilonTransition) + { + // we do the merging + State from = _endArc.Start; + State to = start.End; + done = true; + + if (from == null) + { + // Ignore the current _start _end + Arc.CopyTags(_endArc, start, Direction.Right); + _startArc = start; + } + else if (to == null) + { + // Ignore the old _startArc _endArc + Arc.CopyTags(start, _endArc, Direction.Left); + end = _endArc; + } + else + { + // No tags, just fold the start and end state + if (_endArc.IsPropertylessTransition && start.IsPropertylessTransition) + { + // Move the end arc + start.End = null; + _endArc.Start = null; + _backend.MoveInputTransitionsAndDeleteState(from, to); + } + else + { + // Discard the endstate and replace it with the startArc + Arc.CopyTags(start, _endArc, Direction.Left); + start.End = null; + _endArc.End = to; + } + } + } + } + } + + if (!done) + { + // If the last arc is an epsilon value then there is no need to create a new state + if (_endArc.IsEpsilonTransition && Graph.CanTagsBeMoved(_endArc, start)) + { + // Copy the tags from "endArc" to the "start" + Arc.CopyTags(_endArc, start, Direction.Right); + + if (_endArc.Start != null) + { + // Discard the endstate and replace it with the startArc + state = _endArc.Start; + _endArc.Start = null; + + // Connexion between the state end the start is done below + //state.OutArcs.Add (start); + //start.Start = state; + } + if (_endArc == _startArc) + { + _startArc = start; + } + } + else + { + // If the first arc is an epsilon value then there is no need to create a new state + if (start.IsEpsilonTransition && Graph.CanTagsBeMoved(start, _endArc)) + { + // Copy the tags from "endArc" to the "start" + Arc.CopyTags(start, _endArc, Direction.Left); + + if (start.End != null) + { + // Discard the endstate and replace it with the startArc + state = start.End; + start.End = null; + _endArc.End = state; + state = null; + } + if (start == end) + { + end = _endArc; + } + } + else + { + // Create a new state + state = _backend.CreateNewState(_rule); + + // Connect the new state with the end arc + _endArc.End = state; + } + } + // connect the arcs + if (state != null) + { + start.Start = state; + } + } + _endArc = end; + } + } + + protected Backend _backend; + protected Arc _startArc; + protected Arc _endArc; + } +} diff --git a/src/libraries/System.Speech/src/Internal/SrgsCompiler/PropertyTag.cs b/src/libraries/System.Speech/src/Internal/SrgsCompiler/PropertyTag.cs new file mode 100644 index 00000000000000..28799aa9454afc --- /dev/null +++ b/src/libraries/System.Speech/src/Internal/SrgsCompiler/PropertyTag.cs @@ -0,0 +1,97 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Runtime.InteropServices; +using System.Speech.Internal.SrgsParser; + +namespace System.Speech.Internal.SrgsCompiler +{ + internal sealed class PropertyTag : ParseElement, IPropertyTag + { + #region Constructors + + internal PropertyTag(ParseElement parent, Backend backend) + : base(parent._rule) + { + } + + #endregion + + #region Internal Methods + // The probability that this item will be repeated. + void IPropertyTag.NameValue(IElement parent, string name, object value) + { + //Return if the Tag content is empty + string sValue = value as string; + if (string.IsNullOrEmpty(name) && (value == null || (sValue != null && string.IsNullOrEmpty((sValue).Trim())))) + { + return; + } + + // Build semantic properties to attach to epsilon transition. + // Name= pszValue = null vValue = VT_EMPTY + // Name="string" pszValue = "string" vValue = VT_EMPTY + // Name=true pszValue = null vValue = VT_BOOL + // Name=123 pszValue = null vValue = VT_I4 + // Name=3.14 pszValue = null vValue = VT_R8 + + if (!string.IsNullOrEmpty(name)) + { + // Set property name + _propInfo._pszName = name; + } + else + { + // If no property, set the name to the anonymous property name + _propInfo._pszName = "="; + } + + // Set property value + _propInfo._comValue = value; +#pragma warning disable 0618 // VarEnum is obsolete + if (value == null) + { + _propInfo._comType = VarEnum.VT_EMPTY; + } + else if (sValue != null) + { + _propInfo._comType = VarEnum.VT_EMPTY; + } + else if (value is int) + { + _propInfo._comType = VarEnum.VT_I4; + } + else if (value is double) + { + _propInfo._comType = VarEnum.VT_R8; + } + else if (value is bool) + { + _propInfo._comType = VarEnum.VT_BOOL; + } + else + { + // should never get here + System.Diagnostics.Debug.Assert(false); + } +#pragma warning restore 0618 + } + + void IElement.PostParse(IElement parentElement) + { + ParseElementCollection parent = (ParseElementCollection)parentElement; + _propInfo._ulId = (uint)parent._rule._iSerialize2; + + // Attach the semantic properties on the parent element. + parent.AddSementicPropertyTag(_propInfo); + } + + #endregion + + #region Private Fields + + private CfgGrammar.CfgProperty _propInfo = new(); + + #endregion + } +} diff --git a/src/libraries/System.Speech/src/Internal/SrgsCompiler/Rule.cs b/src/libraries/System.Speech/src/Internal/SrgsCompiler/Rule.cs new file mode 100644 index 00000000000000..0a2eb00b7a299a --- /dev/null +++ b/src/libraries/System.Speech/src/Internal/SrgsCompiler/Rule.cs @@ -0,0 +1,307 @@ +// 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.Diagnostics; +using System.Speech.Internal.SrgsParser; +using System.Text; + +namespace System.Speech.Internal.SrgsCompiler +{ + [DebuggerDisplay("{Name}")] + internal sealed class Rule : ParseElementCollection, IRule, IComparable + { + #region Constructors + + // Only used for the special transition + internal Rule(int iSerialize) + : base(null, null) + { + _iSerialize = iSerialize; + } + + internal Rule(Backend backend, string name, CfgRule cfgRule, int iSerialize, GrammarOptions SemanticFormat, ref int cImportedRules) + : base(backend, null) + { + _rule = this; + Init(name, cfgRule, iSerialize, SemanticFormat, ref cImportedRules); + } + + internal Rule(Backend backend, string name, int offsetName, SPCFGRULEATTRIBUTES attributes, int id, int iSerialize, GrammarOptions SemanticFormat, ref int cImportedRules) + : base(backend, null) + { + _rule = this; + Init(name, new CfgRule(id, offsetName, attributes), iSerialize, SemanticFormat, ref cImportedRules); + } + + #endregion + + #region internal Methods + + #region IComparable Interface implementation + + int IComparable.CompareTo(Rule rule2) + { + Rule rule1 = this; + + if (rule1._cfgRule.Import) + { + return (rule2._cfgRule.Import) ? rule1._cfgRule._nameOffset - rule2._cfgRule._nameOffset : -1; + } + else if (rule1._cfgRule.Dynamic) + { + return (rule2._cfgRule.Dynamic) ? rule1._cfgRule._nameOffset - rule2._cfgRule._nameOffset : 1; + } + else + { + return (rule2._cfgRule.Import) ? 1 : (rule2._cfgRule.Dynamic) ? -1 : rule1._cfgRule._nameOffset - rule2._cfgRule._nameOffset; + } + } + + #endregion + +#if DEBUG + + internal void CheckForExitPath(ref int iRecursiveDepth) + { + if (!_fHasExitPath) + { + // This check allows empty rules. + if (_firstState != null && _firstState.NumArcs != 0) + { + _firstState.CheckExitPath(ref iRecursiveDepth); + } + } + } +#endif + + internal void Validate() + { + if ((!_cfgRule.Dynamic) && (!_cfgRule.Import) && _id != "VOID" && _firstState.NumArcs == 0) + { + XmlParser.ThrowSrgsException(SRID.EmptyRule); + } + else + { + _fHasDynamicRef = _cfgRule.Dynamic; + } + } + + internal void PopulateDynamicRef(ref int iRecursiveDepth) + { + if (iRecursiveDepth > CfgGrammar.MAX_TRANSITIONS_COUNT) + { + XmlParser.ThrowSrgsException((SRID.MaxTransitionsCount)); + } + + foreach (Rule rule in _listRules) + { + if (!rule._fHasDynamicRef) + { + rule._fHasDynamicRef = true; + rule.PopulateDynamicRef(ref iRecursiveDepth); + } + } + } + + internal Rule Clone(StringBlob symbol, string ruleName) + { + Rule rule = new(_iSerialize); + + int idWord; + int offsetName = symbol.Add(ruleName, out idWord); + + rule._id = ruleName; + rule._cfgRule = new CfgRule(idWord, offsetName, _cfgRule._flag) + { + DirtyRule = true, + FirstArcIndex = 0 + }; + return rule; + } + + internal void Serialize(StreamMarshaler streamBuffer) + { + + // Dynamic rules and imports have no arcs + _cfgRule.FirstArcIndex = _firstState != null && !_firstState.OutArcs.IsEmpty ? (uint)_firstState.SerializeId : 0; + + _cfgRule.DirtyRule = true; + + streamBuffer.WriteStream(_cfgRule); + } + + void IElement.PostParse(IElement grammar) + { + // Empty rule + if (_endArc == null) + { + System.Diagnostics.Debug.Assert(_startArc == null); + _firstState = _backend.CreateNewState(this); + } + else + { + // The last arc may contain an epsilon value. Remove it. + TrimEndEpsilons(_endArc, _backend); + + // If the first arc was an epsilon value then there is no need to create a new state + if (_startArc.IsEpsilonTransition && _startArc.End != null && Graph.MoveSemanticTagRight(_startArc)) + { + // Discard the arc and replace it with the startArc + _firstState = _startArc.End; + System.Diagnostics.Debug.Assert(_startArc.End == _startArc.End); + _startArc.End = null; + } + else + { + // if _first has not be set, create it + _firstState = _backend.CreateNewState(this); + + // Attach the start and end arc to the rule + _startArc.Start = _firstState; + } + } + } + + void IRule.CreateScript(IGrammar grammar, string rule, string method, RuleMethodScript type) + { + ((GrammarElement)grammar).CustomGrammar._scriptRefs.Add(new ScriptRef(rule, method, type)); + } + + #endregion + + #region Internal Properties + + internal string Name + { + get + { + return _id; + } + } + + string IRule.BaseClass + { + get + { + return _baseclass; + } + set + { + _baseclass = value; + } + } + + internal string BaseClass + { + get + { + return _baseclass; + } + } + + internal StringBuilder Script + { + get + { + return _script; + } + } + + internal StringBuilder Constructors + { + get + { + return _constructors; + } + } + + #endregion + + #region Private Methods + + private void Init(string id, CfgRule cfgRule, int iSerialize, GrammarOptions SemanticFormat, ref int cImportedRules) + { + _id = id; + _cfgRule = cfgRule; + _firstState = null; + _cfgRule.DirtyRule = true; + _iSerialize = iSerialize; + _fHasExitPath = false; + _fHasDynamicRef = false; + _fIsEpsilonRule = false; + _fStaticRule = false; + if (_cfgRule.Import) + { + cImportedRules++; + } + } + + private static void TrimEndEpsilons(Arc end, Backend backend) + { + Arc endArc = end; + + State endState = endArc.Start; + if (endState != null) + { + // Remove the end arc if possible, check done by MoveSemanticTagRight + if (endArc.IsEpsilonTransition && endState.OutArcs.CountIsOne && Graph.MoveSemanticTagLeft(endArc)) + { + // State has a single input epsilon transition + // Delete the input epsilon transition and delete state. + endArc.Start = null; + + // Remove all the in arcs duplicate the arcs first + foreach (Arc inArc in endState.InArcs.ToList()) + { + inArc.End = null; + TrimEndEpsilons(inArc, backend); + } + + // Delete the input epsilon transition and delete state if appropriate. + backend.DeleteState(endState); + } + } + } + + #endregion + + #region Internal Fields + + internal CfgRule _cfgRule; + + internal State _firstState; + + internal bool _fHasExitPath; + + internal bool _fHasDynamicRef; + + internal bool _fIsEpsilonRule; + + internal int _iSerialize; + internal int _iSerialize2; + +#if DEBUG + internal int _cStates; +#endif + internal List _listRules = new(); + + // this is used to refer to a static rule from a dynamic rule + internal bool _fStaticRule; + + #endregion + + #region Private Fields + + private string _id; + + // STG fields + private string _baseclass; + + private StringBuilder _script = new(); + + private StringBuilder _constructors = new(); + + #endregion + } +} diff --git a/src/libraries/System.Speech/src/Internal/SrgsCompiler/RuleRef.cs b/src/libraries/System.Speech/src/Internal/SrgsCompiler/RuleRef.cs new file mode 100644 index 00000000000000..0d3e3aa49af1f4 --- /dev/null +++ b/src/libraries/System.Speech/src/Internal/SrgsCompiler/RuleRef.cs @@ -0,0 +1,219 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +#region Using directives + +using System.Collections.Generic; +using System.Runtime.InteropServices; +using System.Speech.Internal.SrgsParser; +using System.Text; + +#endregion + +namespace System.Speech.Internal.SrgsCompiler +{ + internal class RuleRef : ParseElement, IRuleRef + { + #region Constructors + + /// + /// Special private constructor for Special Rulerefs + /// + private RuleRef(SpecialRuleRefType type, Rule rule) + : base(rule) + { + _type = type; + } + + /// + /// Add transition corresponding to Special or Uri. + /// + internal RuleRef(ParseElementCollection parent, Backend backend, Uri uri, List undefRules, string semanticKey, string initParameters) + : base(parent._rule) + { + string id = uri.OriginalString; + + Rule ruleRef = null; + int posPound = id.IndexOf('#'); + + // Get the initial state for the RuleRef. + if (posPound == 0) + { + // Internal RuleRef. Get InitialState of RuleRef. + // GetRuleRef() may temporarily create a Rule placeholder for later resolution. + ruleRef = GetRuleRef(backend, id.Substring(1), undefRules); + } + else + { + // External RuleRef. Build URL:GrammarUri#RuleName + StringBuilder sbExternalRuleUri = new("URL:"); + + // Add the parameters to initialize a rule + if (!string.IsNullOrEmpty(initParameters)) + { + // look for the # and insert the parameters + sbExternalRuleUri.Append(posPound > 0 ? id.Substring(0, posPound) : id); + sbExternalRuleUri.Append('>'); + sbExternalRuleUri.Append(initParameters); + if (posPound > 0) + { + sbExternalRuleUri.Append(id.Substring(posPound)); + } + } + else + { + sbExternalRuleUri.Append(id); + } + + // Get InitialState of external RuleRef. + string sExternalRuleUri = sbExternalRuleUri.ToString(); + ruleRef = backend.FindRule(sExternalRuleUri); + if (ruleRef == null) + { + ruleRef = backend.CreateRule(sExternalRuleUri, SPCFGRULEATTRIBUTES.SPRAF_Import); + } + } + Arc rulerefArc = backend.RuleTransition(ruleRef, _rule, 1.0f); +#pragma warning disable 0618 + if (!string.IsNullOrEmpty(semanticKey)) + { + CfgGrammar.CfgProperty propertyInfo = new(); + propertyInfo._pszName = "SemanticKey"; + propertyInfo._comValue = semanticKey; + propertyInfo._comType = VarEnum.VT_EMPTY; + backend.AddPropertyTag(rulerefArc, rulerefArc, propertyInfo); + } +#pragma warning restore 0618 + parent.AddArc(rulerefArc); + } + + #endregion + + #region Internal Method + + /// + /// Returns the initial state of a special rule. + /// For each type of special rule we make a rule with a numeric id and return a reference to it. + /// + internal void InitSpecialRuleRef(Backend backend, ParseElementCollection parent) + { + Rule rule = null; + + // Create a transition corresponding to Special or Uri + switch (_type) + { + case SpecialRuleRefType.Null: + parent.AddArc(backend.EpsilonTransition(1.0f)); + break; + + case SpecialRuleRefType.Void: + rule = backend.FindRule(szSpecialVoid); + if (rule == null) + { + rule = backend.CreateRule(szSpecialVoid, 0); + // Rule with no transitions is a void rule. + ((IRule)rule).PostParse(parent); + } + parent.AddArc(backend.RuleTransition(rule, parent._rule, 1.0f)); + break; + + case SpecialRuleRefType.Garbage: + // Garbage transition is optional whereas Wildcard is not. So we need additional epsilon transition. + OneOf oneOf = new(parent._rule, backend); + // Add the garbage transition + oneOf.AddArc(backend.RuleTransition(CfgGrammar.SPRULETRANS_WILDCARD, parent._rule, 0.5f)); + // Add a parallel epsilon path + oneOf.AddArc(backend.EpsilonTransition(0.5f)); + ((IOneOf)oneOf).PostParse(parent); + break; + + default: + System.Diagnostics.Debug.Assert(false, "Unknown special ruleref type"); + break; + } + } + + #endregion + + #region Private Methods + + /// + /// Return the initial state of the rule with the specified name. + /// If the rule is not defined yet, create a placeholder Rule. + /// + private static Rule GetRuleRef(Backend backend, string sRuleId, List undefRules) + { + System.Diagnostics.Debug.Assert(!string.IsNullOrEmpty(sRuleId)); + + // Get specified rule. + Rule rule = backend.FindRule(sRuleId); + + if (rule == null) + { + // Rule doesn't exist. Create a placeholder rule and add StateHandle to UndefinedRules. + rule = backend.CreateRule(sRuleId, 0); + undefRules.Insert(0, rule); + } + + return rule; + } + + #endregion + + #region internal Properties + + internal static IRuleRef Null + { + get + { + return new RuleRef(SpecialRuleRefType.Null, null); + } + } + + internal static IRuleRef Void + { + get + { + return new RuleRef(SpecialRuleRefType.Void, null); + } + } + internal static IRuleRef Garbage + { + get + { + return new RuleRef(SpecialRuleRefType.Garbage, null); + } + } + + #endregion + + #region Private Fields + + #region Private Enums + // Special rule references allow grammars based on CFGs to have powerful + // additional features, such as transitions into dictation (both recognized + // or not recognized) and word sequences from SAPI 5.0. + private enum SpecialRuleRefType + { + // Defines a rule that is automatically matched that is, matched without + // the user speaking any word. + Null, + // Defines a rule that can never be spoken. Inserting VOID into a sequence + // automatically makes that sequence unspeakable. + Void, + // Defines a rule that may match any speech up until the next rule match, + // the next token or until the end of spoken input. + // Designed for applications that would like to recognize some phrases + // without failing due to irrelevant, or ignorable words. + Garbage, + } + + #endregion + + private SpecialRuleRefType _type; + + private const string szSpecialVoid = "VOID"; + + #endregion + } +} diff --git a/src/libraries/System.Speech/src/Internal/SrgsCompiler/SRGSCompiler.cs b/src/libraries/System.Speech/src/Internal/SrgsCompiler/SRGSCompiler.cs new file mode 100644 index 00000000000000..3acb36d19cee59 --- /dev/null +++ b/src/libraries/System.Speech/src/Internal/SrgsCompiler/SRGSCompiler.cs @@ -0,0 +1,200 @@ +// 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.Globalization; +using System.IO; +using System.Speech.Internal.SrgsParser; +using System.Speech.Recognition.SrgsGrammar; +using System.Text; +using System.Xml; + +namespace System.Speech.Internal.SrgsCompiler +{ + internal static class SrgsCompiler + { + #region Internal Methods + + /// + /// Loads the SRGS XML grammar and produces the binary grammar format. + /// + /// Source SRGS XML streams + /// filename to compile to + /// stream to compile to + /// Compile for CFG or DLL + /// in xmlReader.Count == 1, name of the original file + /// List of referenced assemblies + /// Strong name + internal static void CompileStream(XmlReader[] xmlReaders, string filename, Stream stream, bool fOutputCfg, Uri originalUri, string[] referencedAssemblies, string keyFile) + { + // raft of files to compiler is only available for class library + System.Diagnostics.Debug.Assert(!fOutputCfg || xmlReaders.Length == 1); + + int cReaders = xmlReaders.Length; + List cfgResources = new(); + + CustomGrammar cgCombined = new(); + for (int iReader = 0; iReader < cReaders; iReader++) + { + // Set the current directory to the location where is the grammar + string srgsPath = null; + Uri uri = originalUri; + if (uri == null) + { + if (xmlReaders[iReader].BaseURI != null && xmlReaders[iReader].BaseURI.Length > 0) + { + uri = new Uri(xmlReaders[iReader].BaseURI); + } + } + if (uri != null && (!uri.IsAbsoluteUri || uri.IsFile)) + { + srgsPath = Path.GetDirectoryName(uri.IsAbsoluteUri ? uri.AbsolutePath : uri.OriginalString); + } + + CultureInfo culture; + StringBuilder innerCode = new(); + ISrgsParser srgsParser = new XmlParser(xmlReaders[iReader], uri); + object cg = CompileStream(iReader + 1, srgsParser, srgsPath, filename, stream, fOutputCfg, innerCode, cfgResources, out culture, referencedAssemblies, keyFile); + if (!fOutputCfg) + { + cgCombined.Combine((CustomGrammar)cg, innerCode.ToString()); + } + } + + // Create the DLL if this needs to be done + if (!fOutputCfg) + { + throw new PlatformNotSupportedException(); + } + } + + /// + /// Produces the binary grammar format. + /// + /// Source SRGS XML streams + /// filename to compile to + /// stream to compile to + /// Compile for CFG or DLL + /// List of referenced assemblies + /// Strong name + internal static void CompileStream(SrgsDocument srgsGrammar, string filename, Stream stream, bool fOutputCfg, string[] referencedAssemblies, string keyFile) + { + ISrgsParser srgsParser = new SrgsDocumentParser(srgsGrammar.Grammar); + + List cfgResources = new(); + + StringBuilder innerCode = new(); + CultureInfo culture; + + // Validate the grammar before compiling it. Set the tag-format and sapi flags too. + srgsGrammar.Grammar.Validate(); + + object cg = CompileStream(1, srgsParser, null, filename, stream, fOutputCfg, innerCode, cfgResources, out culture, referencedAssemblies, keyFile); + + // Create the DLL if this needs to be done + if (!fOutputCfg) + { + throw new PlatformNotSupportedException(); + } + } + + #endregion + + private static object CompileStream(int iCfg, ISrgsParser srgsParser, string srgsPath, string filename, Stream stream, bool fOutputCfg, StringBuilder innerCode, object cfgResources, out CultureInfo culture, string[] referencedAssemblies, string keyFile) + { + Backend backend = new(); + CustomGrammar cg = new(); + SrgsElementCompilerFactory elementFactory = new(backend, cg); + srgsParser.ElementFactory = elementFactory; + srgsParser.Parse(); + + // Optimize in-memory graph representation of the grammar. + backend.Optimize(); + culture = backend.LangId == 0x540A ? new CultureInfo("es-us") : new CultureInfo(backend.LangId); + + // A grammar may contains references to other files in codebehind. + // Set the current directory to the location where is the grammar + if (cg._codebehind.Count > 0 && !string.IsNullOrEmpty(srgsPath)) + { + for (int i = 0; i < cg._codebehind.Count; i++) + { + if (!File.Exists(cg._codebehind[i])) + { + cg._codebehind[i] = srgsPath + "\\" + cg._codebehind[i]; + } + } + } + + // Add the referenced assemblies + if (referencedAssemblies != null) + { + foreach (string assembly in referencedAssemblies) + { + cg._assemblyReferences.Add(assembly); + } + } + + // Assign the key file + cg._keyFile = keyFile; + + // Assign the Scripts to the backend + backend.ScriptRefs = cg._scriptRefs; + + // If the target is a dll, then create first the CFG and stuff it as an embedded resource + if (!fOutputCfg) + { + throw new PlatformNotSupportedException(); + } + else + { + //if semantic processing for a rule is defined, a script needs to be defined + if (cg._scriptRefs.Count > 0 && !cg.HasScript) + { + XmlParser.ThrowSrgsException(SRID.NoScriptsForRules); + } + + // Creates a CFG with IL embedded + CreateAssembly(backend, cg); + + // Save binary grammar to dest + if (!string.IsNullOrEmpty(filename)) + { + // Create a stream if a filename was given + stream = new FileStream(filename, FileMode.Create, FileAccess.Write); + } + try + { + using (StreamMarshaler streamHelper = new(stream)) + { + backend.Commit(streamHelper); + } + } + finally + { + if (!string.IsNullOrEmpty(filename)) + { + stream.Close(); + } + } + } + return cg; + } + + /// + /// Generate the assembly code for a back. The scripts are defined in custom + /// grammars. + /// + private static void CreateAssembly(Backend backend, CustomGrammar cg) + { + if (cg.HasScript) + { + throw new PlatformNotSupportedException(); + } + } + } + + internal enum RuleScope + { + PublicRule, PrivateRule + } +} diff --git a/src/libraries/System.Speech/src/Internal/SrgsCompiler/ScriptRef.cs b/src/libraries/System.Speech/src/Internal/SrgsCompiler/ScriptRef.cs new file mode 100644 index 00000000000000..11ecc107c97506 --- /dev/null +++ b/src/libraries/System.Speech/src/Internal/SrgsCompiler/ScriptRef.cs @@ -0,0 +1,71 @@ +// 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; +using System.Speech.Internal.SrgsParser; + +namespace System.Speech.Internal.SrgsCompiler +{ + // list of rules with scripts + [DebuggerDisplay("rule=\"{_rule}\" method=\"{_sMethod}\" operation=\"{_method.ToString ()}\"")] + internal class ScriptRef + { + #region Constructors + + internal ScriptRef(string rule, string sMethod, RuleMethodScript method) + { + _rule = rule; + _sMethod = sMethod; + _method = method; + } + + #endregion + + #region internal Methods + + internal void Serialize(StringBlob symbols, StreamMarshaler streamBuffer) + { + CfgScriptRef script = new(); + + // Get the symbol id for the rule + script._idRule = symbols.Find(_rule); + + script._method = _method; + + script._idMethod = _idSymbol; + + System.Diagnostics.Debug.Assert(script._idRule != -1 && script._idMethod != -1); + + streamBuffer.WriteStream(script); + } + + internal static string OnInitMethod(ScriptRef[] scriptRefs, string rule) + { + if (scriptRefs != null) + { + foreach (ScriptRef script in scriptRefs) + { + if (script._rule == rule && script._method == RuleMethodScript.onInit) + { + return script._sMethod; + } + } + } + return null; + } + + #endregion + + #region Internal Fields + + internal string _rule; + + internal string _sMethod; + + internal RuleMethodScript _method; + + internal int _idSymbol; + + #endregion + } +} diff --git a/src/libraries/System.Speech/src/Internal/SrgsCompiler/SemanticTag.cs b/src/libraries/System.Speech/src/Internal/SrgsCompiler/SemanticTag.cs new file mode 100644 index 00000000000000..2852ef681d508e --- /dev/null +++ b/src/libraries/System.Speech/src/Internal/SrgsCompiler/SemanticTag.cs @@ -0,0 +1,50 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Speech.Internal.SrgsParser; + +namespace System.Speech.Internal.SrgsCompiler +{ + internal sealed class SemanticTag : ParseElement, ISemanticTag + { + #region Constructors + + internal SemanticTag(ParseElement parent, Backend backend) + : base(parent._rule) + { + } + + #endregion + + #region Internal Methods + // The probability that this item will be repeated. + void ISemanticTag.Content(IElement parentElement, string sTag, int iLine) + { + //Return if the Tag content is empty + sTag = sTag.Trim(Helpers._achTrimChars); + + if (string.IsNullOrEmpty(sTag)) + { + return; + } + + // Build semantic properties to attach to epsilon transition. + // script + _propInfo._ulId = (uint)iLine; + _propInfo._comValue = sTag; + + ParseElementCollection parent = (ParseElementCollection)parentElement; + + // Attach the semantic properties on the parent element. + parent.AddSemanticInterpretationTag(_propInfo); + } + + #endregion + + #region Private Fields + + private CfgGrammar.CfgProperty _propInfo = new(); + + #endregion + } +} diff --git a/src/libraries/System.Speech/src/Internal/SrgsCompiler/SrgsElementCompilerFactory.cs b/src/libraries/System.Speech/src/Internal/SrgsCompiler/SrgsElementCompilerFactory.cs new file mode 100644 index 00000000000000..98cf615fc86ba0 --- /dev/null +++ b/src/libraries/System.Speech/src/Internal/SrgsCompiler/SrgsElementCompilerFactory.cs @@ -0,0 +1,360 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +#region Using directives + +using System.Globalization; +using System.Speech.Internal.SrgsParser; + +#endregion + +namespace System.Speech.Internal.SrgsCompiler +{ + internal class SrgsElementCompilerFactory : IElementFactory + { + #region Constructors + + internal SrgsElementCompilerFactory(Backend backend, CustomGrammar cg) + { + _backend = backend; + _cg = cg; + _grammar = new GrammarElement(backend, cg); + } + #endregion + + #region Internal Methods + + /// + /// Clear all the rules + /// + void IElementFactory.RemoveAllRules() + { + } + + IPropertyTag IElementFactory.CreatePropertyTag(IElement parent) + { + return new PropertyTag((ParseElementCollection)parent, _backend); + } + + ISemanticTag IElementFactory.CreateSemanticTag(IElement parent) + { + return new SemanticTag((ParseElementCollection)parent, _backend); + } + + IElementText IElementFactory.CreateText(IElement parent, string value) + { + return null; + } + + IToken IElementFactory.CreateToken(IElement parent, string content, string pronunciation, string display, float reqConfidence) + { + ParseToken((ParseElementCollection)parent, content, pronunciation, display, reqConfidence); + return null; + } + + IItem IElementFactory.CreateItem(IElement parent, IRule rule, int minRepeat, int maxRepeat, float repeatProbability, float weight) + { + return new Item(_backend, (Rule)rule, minRepeat, maxRepeat, repeatProbability, weight); + } + + IRuleRef IElementFactory.CreateRuleRef(IElement parent, Uri srgsUri) + { + throw new NotImplementedException(); + } + + IRuleRef IElementFactory.CreateRuleRef(IElement parent, Uri srgsUri, string semanticKey, string parameters) + { + return new RuleRef((ParseElementCollection)parent, _backend, srgsUri, _grammar.UndefRules, semanticKey, parameters); + } + + void IElementFactory.InitSpecialRuleRef(IElement parent, IRuleRef specialRule) + { + ((RuleRef)specialRule).InitSpecialRuleRef(_backend, (ParseElementCollection)parent); + } + + IOneOf IElementFactory.CreateOneOf(IElement parent, IRule rule) + { + return new OneOf((Rule)rule, _backend); + } + + ISubset IElementFactory.CreateSubset(IElement parent, string text, MatchMode mode) + { + return new Subset((ParseElementCollection)parent, _backend, text, mode); + } + + void IElementFactory.AddScript(IGrammar grammar, string rule, string code) + { + ((GrammarElement)grammar).AddScript(rule, code); + } + + string IElementFactory.AddScript(IGrammar grammar, string rule, string code, string filename, int line) + { + // add the #line information + if (line >= 0) + { + if (_cg._language == "C#") + { + // C# + return string.Format(CultureInfo.InvariantCulture, "#line {0} \"{1}\"\n{2}", line.ToString(CultureInfo.InvariantCulture), filename, code); + } + else + { + // VB.Net + return string.Format(CultureInfo.InvariantCulture, "#ExternalSource (\"{1}\",{0}) \n{2}\n#End ExternalSource\n", line.ToString(CultureInfo.InvariantCulture), filename, code); + } + } + return code; + } + + void IElementFactory.AddScript(IGrammar grammar, string script, string filename, int line) + { + // add the #line information + if (line >= 0) + { + if (_cg._language == "C#") + { + // C# + _cg._script.Append("#line "); + _cg._script.Append(line.ToString(CultureInfo.InvariantCulture)); + _cg._script.Append(" \""); + _cg._script.Append(filename); + _cg._script.Append("\"\n"); + _cg._script.Append(script); + } + else + { + // VB.Net + _cg._script.Append("#ExternalSource ("); + _cg._script.Append(" \""); + _cg._script.Append(filename); + _cg._script.Append("\","); + _cg._script.Append(line.ToString(CultureInfo.InvariantCulture)); + _cg._script.Append(")\n"); + _cg._script.Append(script); + _cg._script.Append("#End #ExternalSource\n"); + } + } + else + { + _cg._script.Append(script); + } + } + + void IElementFactory.AddItem(IOneOf oneOf, IItem item) + { + } + + void IElementFactory.AddElement(IRule rule, IElement value) + { + } + + void IElementFactory.AddElement(IItem item, IElement value) + { + } + + #endregion + + #region Internal Properties + + IGrammar IElementFactory.Grammar + { + get + { + return _grammar; + } + } + + IRuleRef IElementFactory.Null + { + get + { + return RuleRef.Null; + } + } + IRuleRef IElementFactory.Void + { + get + { + return RuleRef.Void; + } + } + IRuleRef IElementFactory.Garbage + { + get + { + return RuleRef.Garbage; + } + } + #endregion + + #region Private Methods + + // Disable parameter validation check + + /// + /// Add transition representing the normalized token. + /// + /// White Space Normalization - Trim leading/trailing white spaces. + /// Collapse white space sequences to a single ' '. + /// Restrictions - Normalized token cannot be empty. + /// Normalized token cannot contain double-quote. + /// + /// If (Parent == Token) And (Parent.SAPIPron.Length > 0) Then + /// Escape normalized token. "/" -> "\/", "\" -> "\\" + /// Build /D/L/P; form from the escaped token and SAPIPron. + /// + /// SAPIPron may be a semi-colon delimited list of pronunciations. + /// In this case, a transition for each of the pronunciations will be added. + /// + /// AddTransition(NormalizedToken, Parent.EndState, NewState) + /// Parent.EndState = NewState + /// + private void ParseToken(ParseElementCollection parent, string sToken, string pronunciation, string display, float reqConfidence) + { + int requiredConfidence = (parent != null) ? parent._confidence : CfgGrammar.SP_NORMAL_CONFIDENCE; + + // Performs white space normalization in place + sToken = Backend.NormalizeTokenWhiteSpace(sToken); + if (string.IsNullOrEmpty(sToken)) + { + return; + } + + // "sapi:reqconf" Attribute + parent._confidence = CfgGrammar.SP_NORMAL_CONFIDENCE; // Default to normal + + if (reqConfidence < 0 || reqConfidence.Equals(0.5f)) + { + parent._confidence = CfgGrammar.SP_NORMAL_CONFIDENCE; // Default to normal + } + else if (reqConfidence < 0.5) + { + parent._confidence = CfgGrammar.SP_LOW_CONFIDENCE; + } + else + { + parent._confidence = CfgGrammar.SP_HIGH_CONFIDENCE; + } + + // If SAPIPron is specified, use /D/L/P; as the transition text, for each of the pronunciations. + if (pronunciation != null || display != null) + { + // Escape normalized token. "/" -> "\/", "\" -> "\\" + string sEscapedToken = EscapeToken(sToken); + string sDisplayToken = display == null ? sEscapedToken : EscapeToken(display); + + if (pronunciation != null) + { + // Garbage transition is optional whereas Wildcard is not. So we need additional epsilon transition. + OneOf oneOf = pronunciation.IndexOf(';') >= 0 ? new OneOf(parent._rule, _backend) : null; + + for (int iCurPron = 0, iDeliminator = 0; iCurPron < pronunciation.Length; iCurPron = iDeliminator + 1) + { + // Find semi-colon delimiter and replace with null + iDeliminator = pronunciation.IndexOf(';', iCurPron); + if (iDeliminator == -1) + { + iDeliminator = pronunciation.Length; + } + + string pron = pronunciation.Substring(iCurPron, iDeliminator - iCurPron); + string sSubPron = null; + switch (_backend.Alphabet) + { + case AlphabetType.Sapi: + sSubPron = PhonemeConverter.ConvertPronToId(pron, _grammar.Backend.LangId); + break; + + case AlphabetType.Ipa: + sSubPron = pron; + PhonemeConverter.ValidateUpsIds(sSubPron); + break; + + case AlphabetType.Ups: + sSubPron = PhonemeConverter.UpsConverter.ConvertPronToId(pron); + break; + } + + // Build /D/L/P; form for this pronunciation. + string sDLP = string.Format(CultureInfo.InvariantCulture, "/{0}/{1}/{2};", sDisplayToken, sEscapedToken, sSubPron); + + // Add /D/L/P; transition to the new state. + if (oneOf != null) + { + oneOf.AddArc(_backend.WordTransition(sDLP, 1.0f, requiredConfidence)); + } + else + { + parent.AddArc(_backend.WordTransition(sDLP, 1.0f, requiredConfidence)); + } + } + + if (oneOf != null) + { + ((IOneOf)oneOf).PostParse(parent); + } + } + else + { + // Build /D/L; form for this pronunciation. + string sDLP = string.Format(CultureInfo.InvariantCulture, "/{0}/{1};", sDisplayToken, sEscapedToken); + + // Add /D/L; transition to the new state. + parent.AddArc(_backend.WordTransition(sDLP, 1.0f, requiredConfidence)); + } + } + else + { + // Add transition to the new state with normalized token. + parent.AddArc(_backend.WordTransition(sToken, 1.0f, requiredConfidence)); + } + } + + /// + /// Escape token. "/" -> "\/", "\" -> "\\" + /// + private static string EscapeToken(string sToken) // String to escape + { + System.Diagnostics.Debug.Assert(!string.IsNullOrEmpty(sToken)); + + // Easy out if no escape characters + if (sToken.IndexOf("\\/", StringComparison.Ordinal) == -1) + { + return sToken; + } + + char[] achSrc = sToken.ToCharArray(); + char[] achDest = new char[achSrc.Length * 2]; + int iDest = 0; + + // Escape slashes and backslashes. + for (int i = 0; i < achSrc.Length;) + { + if ((achSrc[i] == '\\') || (achSrc[i] == '/')) + { + achDest[iDest++] = '\\'; // Escape special character + } + + achDest[iDest++] = achSrc[i++]; + } + + // null terminate and update string length + return new string(achDest, 0, iDest); + } + + #endregion + + #region Private Fields + + // Callers param + private Backend _backend; + + // Grammar + private GrammarElement _grammar; + + // Callers param + private CustomGrammar _cg; + + #endregion + } +} diff --git a/src/libraries/System.Speech/src/Internal/SrgsCompiler/State.cs b/src/libraries/System.Speech/src/Internal/SrgsCompiler/State.cs new file mode 100644 index 00000000000000..36ec36333c5e2e --- /dev/null +++ b/src/libraries/System.Speech/src/Internal/SrgsCompiler/State.cs @@ -0,0 +1,507 @@ +// 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.Diagnostics; +using System.Globalization; +using System.Speech.Internal.SrgsParser; +using System.Text; + +namespace System.Speech.Internal.SrgsCompiler +{ + /// + /// Class representing a state in the grammar. Note that states are not stored in the binary format + /// instead all the arcs are, with a flag to indicate the end arc out of a state */ + /// +#if DEBUG + [DebuggerDisplay("{ToString ()}")] +#endif + internal sealed class State : IComparable + { + #region Constructors + + internal State(Rule rule, uint hState, int iSerialize) + { + _rule = rule; + _iSerialize = iSerialize; + _id = hState; + } + + internal State(Rule rule, uint hState) + : this(rule, hState, (int)hState) + { + } + + #endregion + + #region internal Methods + + #region IComparable Interface implementation + + int IComparable.CompareTo(State state2) + { + return Compare(this, state2); + } + + #endregion + + internal void SerializeStateEntries(StreamMarshaler streamBuffer, bool tagsCannotSpanOverMultipleArcs, float[] pWeights, ref uint iArcOffset, ref int iOffset) + { + // The arcs must be sorted before being written to disk. + List outArcs = _outArcs.ToList(); + outArcs.Sort(); + Arc lastArc = outArcs.Count > 0 ? outArcs[outArcs.Count - 1] : null; + + IEnumerator enumArcs = ((IEnumerable)outArcs).GetEnumerator(); + enumArcs.MoveNext(); + + uint nextAvailableArc = (uint)outArcs.Count + iArcOffset; + uint saveNextAvailableArc = nextAvailableArc; + + // Write the arc of the first epsilon arc with an arc has more than one semantic tag + foreach (Arc arc in outArcs) + { + // Create the first arc. + int cSemantics = arc.SemanticTagCount; + + // Set the semantic property reference for the first arc + if (cSemantics > 0) + { + arc.SetArcIndexForTag(0, iArcOffset, tagsCannotSpanOverMultipleArcs); + } + + // Serialize the arc + if (cSemantics <= 1) + { + pWeights[iOffset++] = arc.Serialize(streamBuffer, lastArc == arc, iArcOffset++); + } + else + { + // update the position of the current arc + ++iArcOffset; + + // more than one arc, create an epsilon transition + pWeights[iOffset++] = Arc.SerializeExtraEpsilonWithTag(streamBuffer, arc, lastArc == arc, nextAvailableArc); + + // reset the position of the next available slop for an arc + nextAvailableArc += (uint)cSemantics - 1; + } + } + + enumArcs = ((IEnumerable)outArcs).GetEnumerator(); + enumArcs.MoveNext(); + + // revert the position for the new arc + nextAvailableArc = saveNextAvailableArc; + + // write the additional arcs if we have more than one semantic tag + foreach (Arc arc in outArcs) + { + int cSemantics = arc.SemanticTagCount; + + if (cSemantics > 1) + { + // If more than 2 arcs insert extra new epsilon states, one per semantic tag + for (int i = 1; i < cSemantics - 1; i++) + { + // Set the semantic property reference + arc.SetArcIndexForTag(i, iArcOffset, tagsCannotSpanOverMultipleArcs); + + // reset the position of the next available slop for an arc + nextAvailableArc++; + + // create an epsilon transition + pWeights[iOffset++] = Arc.SerializeExtraEpsilonWithTag(streamBuffer, arc, true, nextAvailableArc); + + // update the position of the current arc + ++iArcOffset; + } + + // Set the semantic property reference + arc.SetArcIndexForTag(cSemantics - 1, iArcOffset, tagsCannotSpanOverMultipleArcs); + + // Add the real arc at the end + pWeights[iOffset++] = arc.Serialize(streamBuffer, true, iArcOffset++); + + // reset the position of the next available slop for an arc + nextAvailableArc++; + } + } + } + + internal void SetEndArcIndexForTags() + { + foreach (Arc arc in _outArcs) + { + arc.SetEndArcIndexForTags(); + } + } + + #region State linked list + + // The pointers for 2 linked list are stored within each state. + // When states are created, they added into a list, the '1' list. + + // The Members of the list are Set, Add, Remove, Prev and Next. + + internal void Init() + { + System.Diagnostics.Debug.Assert(_next == null && _prev == null); + } + + internal State Add(State state) + { + _next = state; + state._prev = this; + return state; + } + + internal void Remove() + { + if (_prev != null) + { + _prev._next = _next; + } + if (_next != null) + { + _next._prev = _prev; + } + _next = _prev = null; + } + + internal State Next + { + get + { + return _next; + } + } + + internal State Prev + { + get + { + return _prev; + } + } + + #endregion + +#if DEBUG + internal void CheckExitPath(ref int iRecursiveDepth) + { + if (iRecursiveDepth > CfgGrammar.MAX_TRANSITIONS_COUNT) + { + XmlParser.ThrowSrgsException(SRID.MaxTransitionsCount); + } + + foreach (Arc arc in _outArcs) + { + if (_rule._fHasExitPath) + { + break; + } + + if (arc.CheckingForExitPath) + { + arc.CheckingForExitPath = true; + if (arc.RuleRef != null) + { + arc.RuleRef.CheckForExitPath(ref iRecursiveDepth); + if (arc.RuleRef._fHasExitPath) + { + if (arc.End == null) + { + _rule._fHasExitPath = true; + } + else + { + arc.End.CheckExitPath(ref iRecursiveDepth); + } + } + } + else + { + if (arc.End == null) + { + _rule._fHasExitPath = true; + } + else + { + arc.End.CheckExitPath(ref iRecursiveDepth); + } + } + + arc.CheckingForExitPath = false; + } + } + } +#endif + + internal void CheckLeftRecursion(out bool fReachedEndState) + { + fReachedEndState = false; + if ((int)(_recurseFlag & RecurFlag.RF_IN_LEFT_RECUR_CHECK) != 0) + { + XmlParser.ThrowSrgsException(SRID.CircularRuleRef, _rule != null ? _rule._rule.Name : string.Empty); + } + else + { + if ((_recurseFlag & RecurFlag.RF_CHECKED_LEFT_RECURSION) == 0) + { + _recurseFlag |= RecurFlag.RF_CHECKED_LEFT_RECURSION | RecurFlag.RF_IN_LEFT_RECUR_CHECK; + foreach (Arc arc in _outArcs) + { + bool fRuleReachedEndState = false; // Does the rule ref have epsilon path to the end? + + // Traverse any rule refs to check for circular rule reference. + if (arc.RuleRef != null && arc.RuleRef._firstState != null) + { + State pRuleFirstNode = arc.RuleRef._firstState; + + if (((int)(pRuleFirstNode._recurseFlag & RecurFlag.RF_IN_LEFT_RECUR_CHECK) != 0) || // Circular RuleRef + ((int)(pRuleFirstNode._recurseFlag & RecurFlag.RF_CHECKED_LEFT_RECURSION) == 0)) // Untraversed rule + { + pRuleFirstNode.CheckLeftRecursion(out fRuleReachedEndState); + } + else + { + fRuleReachedEndState = arc.RuleRef._fIsEpsilonRule; + } + } + + // Can transition be traversed by epsilon? + if (fRuleReachedEndState || ((arc.RuleRef == null) && (arc.WordId == 0) && arc.WordId == 0)) + { + if (arc.End != null) + { + arc.End.CheckLeftRecursion(out fReachedEndState); + } + else + { + fReachedEndState = true; + } + } + } + + _recurseFlag &= (~RecurFlag.RF_IN_LEFT_RECUR_CHECK); + if ((_rule._firstState == this) && fReachedEndState) + { + _rule._fIsEpsilonRule = true; + } + } + } + } + + #endregion + + #region Internal Properties + + internal int NumArcs + { + get + { + // if the number of tags > 1 extra epsilon state needs to be inserted + int cExtra = 0; + foreach (Arc arc in _outArcs) + { + if (arc.SemanticTagCount > 0) + { + cExtra += arc.SemanticTagCount - 1; + } + } + + int cArcs = 0; + foreach (Arc arc in _outArcs) + { + cArcs++; + } + return cArcs + cExtra; + } + } + + internal int NumSemanticTags + { + get + { + int c = 0; + + foreach (Arc arc in _outArcs) + { + c += arc.SemanticTagCount; + } + + return c; + } + } + + internal Rule Rule + { + get + { + return _rule; + } + } + + internal uint Id + { + get + { + return _id; + } + } + + internal ArcList OutArcs + { + get + { + return _outArcs; + } + } + + internal ArcList InArcs + { + get + { + return _inArcs; + } + } + + internal int SerializeId + { + get + { + return _iSerialize; + } + set + { + _iSerialize = value; + } + } + + #endregion + + #region private Methods + + // Sort based on rule first, so all states, and arcs for a rule end up together. + // Then sort on index. + private static int Compare(State state1, State state2) + { + if (state1._rule._cfgRule._nameOffset != state2._rule._cfgRule._nameOffset) + { + return state1._rule._cfgRule._nameOffset - state2._rule._cfgRule._nameOffset; + } + else + { + // First state of a rule needs to be in front. + int isNode1FirstNode = (state1._rule._firstState == state1) ? -1 : 0; + int isNode2FirstNode = (state2._rule._firstState == state2) ? -1 : 0; + + if (isNode1FirstNode != isNode2FirstNode) + { + return isNode1FirstNode - isNode2FirstNode; + } + else + { + // First returns null on empty collections + Arc arc1 = state1._outArcs != null && !state1._outArcs.IsEmpty ? state1._outArcs.First : null; + Arc arc2 = state2._outArcs != null && !state2._outArcs.IsEmpty ? state2._outArcs.First : null; + + int diff = (arc1 != null ? (arc1.RuleRef != null ? 0x1000000 : 0) + arc1.WordId : state1._iSerialize) - (arc2 != null ? (arc2.RuleRef != null ? 0x1000000 : 0) + arc2.WordId : state2._iSerialize); + + diff = diff != 0 ? diff : state1._iSerialize - state2._iSerialize; + //System.Diagnostics.Debug.Assert (diff != 0); + return diff; + } + } + } + +#if DEBUG + + public override string ToString() + { + StringBuilder sb = new("[#"); + sb.Append(_id.ToString(CultureInfo.InvariantCulture)); + if (_rule != null && _rule._firstState == this) + { + sb.Append(' '); + sb.Append(_rule.Name); + } + sb.Append("] "); + if (_inArcs != null) + { + bool first = true; + foreach (Arc arc in _inArcs) + { + if (!first) + { + sb.Append("\x20\x25cf\x20"); + } + sb.Append('#'); + sb.Append(arc.Start != null ? arc.Start._id.ToString(CultureInfo.InvariantCulture) : "S"); + sb.Append(' '); + sb.Append(arc.DebuggerDisplayTags()); + first = false; + } + } + sb.Append(" <--> "); + if (_outArcs != null) + { + bool first = true; + foreach (Arc arc in _outArcs) + { + if (!first) + { + sb.Append("\x20\x25cf\x20"); + } + sb.Append('#'); + sb.Append(arc.End != null ? arc.End._id.ToString(CultureInfo.InvariantCulture) : "E"); + sb.Append(' '); + sb.Append(arc.DebuggerDisplayTags()); + first = false; + } + } + + return sb.ToString(); + } +#endif + + #endregion + + #region internal Fields + +#pragma warning disable 56524 // Arclist does not hold on any resources + + // Collection of transitions leaving this state + private ArcList _outArcs = new(); + + // Collection of transitions entering this state + private ArcList _inArcs = new(); + +#pragma warning restore 56524 // Arclist does not hold on any resources + + // Index of the first arc in the state. Also used as the state handle in SR engine interfaces. + private int _iSerialize; + + private uint _id; + + private Rule _rule; + + private State _next; + private State _prev; + + // Flags used for recursive validation methods + internal enum RecurFlag : uint + { + RF_CHECKED_EPSILON = (1 << 0), + RF_CHECKED_EXIT_PATH = (1 << 1), + RF_CHECKED_LEFT_RECURSION = (1 << 2), + RF_IN_LEFT_RECUR_CHECK = (1 << 3) + }; + + // Flags used by recursive algorithms + private RecurFlag _recurseFlag; + + #endregion + } +} diff --git a/src/libraries/System.Speech/src/Internal/SrgsCompiler/Subset.cs b/src/libraries/System.Speech/src/Internal/SrgsCompiler/Subset.cs new file mode 100644 index 00000000000000..ea642f57567119 --- /dev/null +++ b/src/libraries/System.Speech/src/Internal/SrgsCompiler/Subset.cs @@ -0,0 +1,48 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +#region Using directives + +using System.Speech.Internal.SrgsParser; + +#endregion + +namespace System.Speech.Internal.SrgsCompiler +{ + internal class Subset : ParseElement, ISubset + { + #region Constructors + + /// + /// Process the 'subset' element. + /// + public Subset(ParseElementCollection parent, Backend backend, string text, MatchMode mode) + : base(parent._rule) + { + // replace tab, cr, lf with spaces + foreach (char ch in Helpers._achTrimChars) + { + if (ch == ' ') + { + continue; + } + if (text.IndexOf(ch) >= 0) + { + text = text.Replace(ch, ' '); + } + } + + // Add transition to the new state with normalized token. + parent.AddArc(backend.SubsetTransition(text, mode)); + } + + #endregion + + #region Internal Method + void IElement.PostParse(IElement parentElement) + { + } + + #endregion + } +} diff --git a/src/libraries/System.Speech/src/Internal/SrgsCompiler/Tag.cs b/src/libraries/System.Speech/src/Internal/SrgsCompiler/Tag.cs new file mode 100644 index 00000000000000..31118471a27987 --- /dev/null +++ b/src/libraries/System.Speech/src/Internal/SrgsCompiler/Tag.cs @@ -0,0 +1,61 @@ +// 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; + +namespace System.Speech.Internal.SrgsCompiler +{ +#if DEBUG + [DebuggerDisplay("{_be.Symbols.FromOffset (_cfgTag._nameOffset == 0 ? _cfgTag._valueOffset : _cfgTag._nameOffset)}")] +#endif + internal sealed class Tag : IComparable + { + #region Constructors + + internal Tag(Tag tag) + { + _be = tag._be; + _cfgTag = tag._cfgTag; + } + + internal Tag(Backend be, CfgSemanticTag cfgTag) + { + _be = be; + _cfgTag = cfgTag; + } + + internal Tag(Backend be, CfgGrammar.CfgProperty property) + { + _be = be; + _cfgTag = new CfgSemanticTag(be.Symbols, property); + } + + #endregion + + #region Internal Methods + + #region IComparable Interface implementation + + int IComparable.CompareTo(Tag tag) + { + return (int)_cfgTag.ArcIndex - (int)tag._cfgTag.ArcIndex; + } + + #endregion + + internal void Serialize(StreamMarshaler streamBuffer) + { + streamBuffer.WriteStream(_cfgTag); + } + + #endregion + + #region Internal Fields + + internal CfgSemanticTag _cfgTag; + + internal Backend _be; + + #endregion + } +} diff --git a/src/libraries/System.Speech/src/Internal/SrgsParser/IElement.cs b/src/libraries/System.Speech/src/Internal/SrgsParser/IElement.cs new file mode 100644 index 00000000000000..fe063e58292c26 --- /dev/null +++ b/src/libraries/System.Speech/src/Internal/SrgsParser/IElement.cs @@ -0,0 +1,13 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace System.Speech.Internal.SrgsParser +{ + /// + /// Interface definition for the IElement + /// + internal interface IElement + { + void PostParse(IElement parent); + } +} diff --git a/src/libraries/System.Speech/src/Internal/SrgsParser/IElementFactory.cs b/src/libraries/System.Speech/src/Internal/SrgsParser/IElementFactory.cs new file mode 100644 index 00000000000000..6b2bf2bad868fd --- /dev/null +++ b/src/libraries/System.Speech/src/Internal/SrgsParser/IElementFactory.cs @@ -0,0 +1,39 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace System.Speech.Internal.SrgsParser +{ + /// + /// Interface definition for the IElementFactory + /// + internal interface IElementFactory + { + // Grammar + void RemoveAllRules(); + + IElementText CreateText(IElement parent, string value); + IToken CreateToken(IElement parent, string content, string pronumciation, string display, float reqConfidence); + IPropertyTag CreatePropertyTag(IElement parent); + ISemanticTag CreateSemanticTag(IElement parent); + IItem CreateItem(IElement parent, IRule rule, int minRepeat, int maxRepeat, float repeatProbability, float weight); + IRuleRef CreateRuleRef(IElement parent, Uri srgsUri); + IRuleRef CreateRuleRef(IElement parent, Uri srgsUri, string semanticKey, string parameters); + void InitSpecialRuleRef(IElement parent, IRuleRef special); + IOneOf CreateOneOf(IElement parent, IRule rule); + ISubset CreateSubset(IElement parent, string text, MatchMode matchMode); + + IGrammar Grammar { get; } + + IRuleRef Null { get; } + IRuleRef Void { get; } + IRuleRef Garbage { get; } + + string AddScript(IGrammar grammar, string rule, string code, string filename, int line); + void AddScript(IGrammar grammar, string script, string filename, int line); + void AddScript(IGrammar grammar, string rule, string code); + + void AddItem(IOneOf oneOf, IItem value); + void AddElement(IRule rule, IElement value); + void AddElement(IItem item, IElement value); + } +} diff --git a/src/libraries/System.Speech/src/Internal/SrgsParser/IElementText.cs b/src/libraries/System.Speech/src/Internal/SrgsParser/IElementText.cs new file mode 100644 index 00000000000000..c9b275716485c4 --- /dev/null +++ b/src/libraries/System.Speech/src/Internal/SrgsParser/IElementText.cs @@ -0,0 +1,12 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace System.Speech.Internal.SrgsParser +{ + /// + /// Interface definition for the IElementText + /// + internal interface IElementText : IElement + { + } +} diff --git a/src/libraries/System.Speech/src/Internal/SrgsParser/IGrammar.cs b/src/libraries/System.Speech/src/Internal/SrgsParser/IGrammar.cs new file mode 100644 index 00000000000000..00decfc3dd2f59 --- /dev/null +++ b/src/libraries/System.Speech/src/Internal/SrgsParser/IGrammar.cs @@ -0,0 +1,36 @@ +// 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; + +namespace System.Speech.Internal.SrgsParser +{ + /// + /// Interface definition for the IGrammar + /// + internal interface IGrammar : IElement + { + IRule CreateRule(string id, RulePublic publicRule, RuleDynamic dynamic, bool hasSCript); + + string Root { get; set; } + System.Speech.Recognition.SrgsGrammar.SrgsTagFormat TagFormat { get; set; } + Collection GlobalTags { get; set; } + GrammarType Mode { set; } + CultureInfo Culture { set; } + Uri XmlBase { set; } + AlphabetType PhoneticAlphabet { set; } + + string Language { get; set; } + string Namespace { get; set; } + bool Debug { set; } + Collection CodeBehind { get; set; } + Collection ImportNamespaces { get; set; } + Collection AssemblyReferences { get; set; } + } + + internal enum GrammarType + { + VoiceGrammar, DtmfGrammar + } +} diff --git a/src/libraries/System.Speech/src/Internal/SrgsParser/IItem.cs b/src/libraries/System.Speech/src/Internal/SrgsParser/IItem.cs new file mode 100644 index 00000000000000..9620f11a57b2c0 --- /dev/null +++ b/src/libraries/System.Speech/src/Internal/SrgsParser/IItem.cs @@ -0,0 +1,12 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace System.Speech.Internal.SrgsParser +{ + /// + /// Interface definition for the IItem + /// + internal interface IItem : IElement + { + } +} diff --git a/src/libraries/System.Speech/src/Internal/SrgsParser/IOneOf.cs b/src/libraries/System.Speech/src/Internal/SrgsParser/IOneOf.cs new file mode 100644 index 00000000000000..e344ab0f1d6d0f --- /dev/null +++ b/src/libraries/System.Speech/src/Internal/SrgsParser/IOneOf.cs @@ -0,0 +1,12 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace System.Speech.Internal.SrgsParser +{ + /// + /// Interface definition for the IOneOf + /// + internal interface IOneOf : IElement + { + } +} diff --git a/src/libraries/System.Speech/src/Internal/SrgsParser/IPropertyTag.cs b/src/libraries/System.Speech/src/Internal/SrgsParser/IPropertyTag.cs new file mode 100644 index 00000000000000..9a7256c0d57a2b --- /dev/null +++ b/src/libraries/System.Speech/src/Internal/SrgsParser/IPropertyTag.cs @@ -0,0 +1,13 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace System.Speech.Internal.SrgsParser +{ + /// + /// Interface definition for the IElementTag + /// + internal interface IPropertyTag : IElement + { + void NameValue(IElement parent, string name, object value); + } +} diff --git a/src/libraries/System.Speech/src/Internal/SrgsParser/IRule.cs b/src/libraries/System.Speech/src/Internal/SrgsParser/IRule.cs new file mode 100644 index 00000000000000..2e2b0251b2d3d1 --- /dev/null +++ b/src/libraries/System.Speech/src/Internal/SrgsParser/IRule.cs @@ -0,0 +1,30 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace System.Speech.Internal.SrgsParser +{ + internal interface IRule : IElement + { + string BaseClass { get; set; } + + void CreateScript(IGrammar grammar, string rule, string method, RuleMethodScript type); + } + + #region Internal Enums + + internal enum RuleDynamic + { + True, + False, + NotSet + }; + + internal enum RulePublic + { + True, + False, + NotSet + }; + + #endregion +} diff --git a/src/libraries/System.Speech/src/Internal/SrgsParser/IRuleRef.cs b/src/libraries/System.Speech/src/Internal/SrgsParser/IRuleRef.cs new file mode 100644 index 00000000000000..21ca321d591a99 --- /dev/null +++ b/src/libraries/System.Speech/src/Internal/SrgsParser/IRuleRef.cs @@ -0,0 +1,12 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace System.Speech.Internal.SrgsParser +{ + /// + /// Interface definition for the IRuleRef + /// + internal interface IRuleRef : IElement + { + } +} diff --git a/src/libraries/System.Speech/src/Internal/SrgsParser/IScript.cs b/src/libraries/System.Speech/src/Internal/SrgsParser/IScript.cs new file mode 100644 index 00000000000000..fa1eefc623aeeb --- /dev/null +++ b/src/libraries/System.Speech/src/Internal/SrgsParser/IScript.cs @@ -0,0 +1,21 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace System.Speech.Internal.SrgsParser +{ + /// + /// Interface definition for the IScript + /// + internal interface IScript : IElement + { + IScript Create(string rule, RuleMethodScript onInit); + } + + internal enum RuleMethodScript + { + onInit = 1, + onParse = 2, + onRecognition = 3, + onError + } +} diff --git a/src/libraries/System.Speech/src/Internal/SrgsParser/ISemanticTag.cs b/src/libraries/System.Speech/src/Internal/SrgsParser/ISemanticTag.cs new file mode 100644 index 00000000000000..3aa4a9aa763356 --- /dev/null +++ b/src/libraries/System.Speech/src/Internal/SrgsParser/ISemanticTag.cs @@ -0,0 +1,13 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace System.Speech.Internal.SrgsParser +{ + /// + /// Interface definition for the IElementTag + /// + internal interface ISemanticTag : IElement + { + void Content(IElement parent, string value, int line); + } +} diff --git a/src/libraries/System.Speech/src/Internal/SrgsParser/ISrgsParser.cs b/src/libraries/System.Speech/src/Internal/SrgsParser/ISrgsParser.cs new file mode 100644 index 00000000000000..2703a36b5d73b4 --- /dev/null +++ b/src/libraries/System.Speech/src/Internal/SrgsParser/ISrgsParser.cs @@ -0,0 +1,11 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace System.Speech.Internal.SrgsParser +{ + internal interface ISrgsParser + { + void Parse(); + IElementFactory ElementFactory { set; } + } +} diff --git a/src/libraries/System.Speech/src/Internal/SrgsParser/ISubset.cs b/src/libraries/System.Speech/src/Internal/SrgsParser/ISubset.cs new file mode 100644 index 00000000000000..7963d26b3eeeb4 --- /dev/null +++ b/src/libraries/System.Speech/src/Internal/SrgsParser/ISubset.cs @@ -0,0 +1,22 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace System.Speech.Internal.SrgsParser +{ + /// + /// Interface definition for the ISubset + /// + internal interface ISubset : IElement + { + } + + // Must be in the same order as the Srgs enum + internal enum MatchMode + { + AllWords = 0, + Subsequence = 1, + OrderedSubset = 3, + SubsequenceContentRequired = 5, + OrderedSubsetContentRequired = 7 + } +} diff --git a/src/libraries/System.Speech/src/Internal/SrgsParser/IToken.cs b/src/libraries/System.Speech/src/Internal/SrgsParser/IToken.cs new file mode 100644 index 00000000000000..dd180eb4121ca9 --- /dev/null +++ b/src/libraries/System.Speech/src/Internal/SrgsParser/IToken.cs @@ -0,0 +1,17 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace System.Speech.Internal.SrgsParser +{ + /// + /// Interface definition for the IToken + /// + internal interface IToken : IElement + { + string Text { set; } + string Display { set; } + string Pronunciation { set; } + } + + internal delegate IToken CreateTokenCallback(IElement parent, string content, string pronumciation, string display, float reqConfidence); +} diff --git a/src/libraries/System.Speech/src/Internal/SrgsParser/SrgsDocumentParser.cs b/src/libraries/System.Speech/src/Internal/SrgsParser/SrgsDocumentParser.cs new file mode 100644 index 00000000000000..66477e9c9cd778 --- /dev/null +++ b/src/libraries/System.Speech/src/Internal/SrgsParser/SrgsDocumentParser.cs @@ -0,0 +1,423 @@ +// 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.Speech.Recognition; +using System.Speech.Recognition.SrgsGrammar; + +namespace System.Speech.Internal.SrgsParser +{ + internal class SrgsDocumentParser : ISrgsParser + { + #region Constructors + + internal SrgsDocumentParser(SrgsGrammar grammar) + { + _grammar = grammar; + } + + #endregion + + #region Internal Methods + + // Initializes the object from a stream containing SRGS in XML + public void Parse() + { + try + { + ProcessGrammarElement(_grammar, _parser.Grammar); + } + catch + { + // clear all the rules + _parser.RemoveAllRules(); + throw; + } + } + + #endregion + + #region Internal Properties + + public IElementFactory ElementFactory + { + set + { + _parser = value; + } + } + + #endregion + + #region Private Methods + + /// + /// Process the top level grammar element + /// + private void ProcessGrammarElement(SrgsGrammar source, IGrammar grammar) + { + grammar.Culture = source.Culture; + grammar.Mode = source.Mode; + if (source.Root != null) + { + grammar.Root = source.Root.Id; + } + grammar.TagFormat = source.TagFormat; + grammar.XmlBase = source.XmlBase; + grammar.GlobalTags = source.GlobalTags; + grammar.PhoneticAlphabet = source.PhoneticAlphabet; + + // Process child elements. + foreach (SrgsRule srgsRule in source.Rules) + { + IRule rule = ParseRule(grammar, srgsRule); + rule.PostParse(grammar); + } + grammar.AssemblyReferences = source.AssemblyReferences; + grammar.CodeBehind = source.CodeBehind; + grammar.Debug = source.Debug; + grammar.ImportNamespaces = source.ImportNamespaces; + grammar.Language = source.Language == null ? "C#" : source.Language; + grammar.Namespace = source.Namespace; + + // if add the content to the generic _scrip + _parser.AddScript(grammar, source.Script, null, -1); + // Finish all initialization - should check for the Root and the all + // rules are defined + grammar.PostParse(null); + } + + /// + /// Parse a rule + /// + private IRule ParseRule(IGrammar grammar, SrgsRule srgsRule) + { + string id = srgsRule.Id; + bool hasScript = srgsRule.OnInit != null || srgsRule.OnParse != null || srgsRule.OnError != null || srgsRule.OnRecognition != null; + IRule rule = grammar.CreateRule(id, srgsRule.Scope == SrgsRuleScope.Public ? RulePublic.True : RulePublic.False, srgsRule.Dynamic, hasScript); + + if (srgsRule.OnInit != null) + { + rule.CreateScript(grammar, id, srgsRule.OnInit, RuleMethodScript.onInit); + } + + if (srgsRule.OnParse != null) + { + rule.CreateScript(grammar, id, srgsRule.OnParse, RuleMethodScript.onParse); + } + + if (srgsRule.OnError != null) + { + rule.CreateScript(grammar, id, srgsRule.OnError, RuleMethodScript.onError); + } + + if (srgsRule.OnRecognition != null) + { + rule.CreateScript(grammar, id, srgsRule.OnRecognition, RuleMethodScript.onRecognition); + } + + // Add the code to the backend + if (srgsRule.Script.Length > 0) + { + _parser.AddScript(grammar, id, srgsRule.Script); + } + + rule.BaseClass = srgsRule.BaseClass; + + foreach (SrgsElement srgsElement in GetSortedTagElements(srgsRule.Elements)) + { + ProcessChildNodes(srgsElement, rule, rule); + } + return rule; + } + + /// + /// Parse a ruleref + /// + private IRuleRef ParseRuleRef(SrgsRuleRef srgsRuleRef, IElement parent) + { + IRuleRef ruleRef = null; + bool fSpecialRuleRef = true; + + if (srgsRuleRef == SrgsRuleRef.Null) + { + ruleRef = _parser.Null; + } + else if (srgsRuleRef == SrgsRuleRef.Void) + { + ruleRef = _parser.Void; + } + else if (srgsRuleRef == SrgsRuleRef.Garbage) + { + ruleRef = _parser.Garbage; + } + else + { + ruleRef = _parser.CreateRuleRef(parent, srgsRuleRef.Uri, srgsRuleRef.SemanticKey, srgsRuleRef.Params); + fSpecialRuleRef = false; + } + + if (fSpecialRuleRef) + { + _parser.InitSpecialRuleRef(parent, ruleRef); + } + + ruleRef.PostParse(parent); + return ruleRef; + } + + /// + /// Parse a One-Of + /// + private IOneOf ParseOneOf(SrgsOneOf srgsOneOf, IElement parent, IRule rule) + { + IOneOf oneOf = _parser.CreateOneOf(parent, rule); + + // Process child elements. + foreach (SrgsItem item in srgsOneOf.Items) + { + ProcessChildNodes(item, oneOf, rule); + } + oneOf.PostParse(parent); + return oneOf; + } + + /// + /// Parse Item + /// + private IItem ParseItem(SrgsItem srgsItem, IElement parent, IRule rule) + { + IItem item = _parser.CreateItem(parent, rule, srgsItem.MinRepeat, srgsItem.MaxRepeat, srgsItem.RepeatProbability, srgsItem.Weight); + + // Process child elements. + foreach (SrgsElement srgsElement in GetSortedTagElements(srgsItem.Elements)) + { + ProcessChildNodes(srgsElement, item, rule); + } + + item.PostParse(parent); + return item; + } + + /// + /// Parse Token + /// + private IToken ParseToken(SrgsToken srgsToken, IElement parent) + { + return _parser.CreateToken(parent, srgsToken.Text, srgsToken.Pronunciation, srgsToken.Display, -1); + } + + /// + /// Break the string into individual tokens and ParseToken() each individual token. + /// + /// Token string is a sequence of 0 or more white space delimited tokens. + /// Tokens may also be delimited by double quotes. In these cases, the double + /// quotes token must be surrounded by white space or string boundary. + /// + private void ParseText(IElement parent, string sChars, string pronunciation, string display, float reqConfidence) + { + System.Diagnostics.Debug.Assert((parent != null) && (!string.IsNullOrEmpty(sChars))); + + XmlParser.ParseText(parent, sChars, pronunciation, display, reqConfidence, new CreateTokenCallback(_parser.CreateToken)); + } + + /// + /// Parse tag + /// + private ISubset ParseSubset(SrgsSubset srgsSubset, IElement parent) + { + MatchMode matchMode = MatchMode.Subsequence; + + switch (srgsSubset.MatchingMode) + { + case SubsetMatchingMode.OrderedSubset: + matchMode = MatchMode.OrderedSubset; + break; + + case SubsetMatchingMode.OrderedSubsetContentRequired: + matchMode = MatchMode.OrderedSubsetContentRequired; + break; + + case SubsetMatchingMode.Subsequence: + matchMode = MatchMode.Subsequence; + break; + + case SubsetMatchingMode.SubsequenceContentRequired: + matchMode = MatchMode.SubsequenceContentRequired; + break; + } + return _parser.CreateSubset(parent, srgsSubset.Text, matchMode); + } + + /// + /// Parse tag + /// + private ISemanticTag ParseSemanticTag(SrgsSemanticInterpretationTag srgsTag, IElement parent) + { + ISemanticTag tag = _parser.CreateSemanticTag(parent); + + tag.Content(parent, srgsTag.Script, 0); + tag.PostParse(parent); + return tag; + } + + /// + /// ParseNameValueTag tag + /// + private IPropertyTag ParseNameValueTag(SrgsNameValueTag srgsTag, IElement parent) + { + IPropertyTag tag = _parser.CreatePropertyTag(parent); + + // Initialize the tag + tag.NameValue(parent, srgsTag.Name, srgsTag.Value); + + // Since the tag are always pushed at the end of the element list, they can be committed right away + tag.PostParse(parent); + return tag; + } + + /// + /// Calls the appropriate Parsing function based on the element type + /// + private void ProcessChildNodes(SrgsElement srgsElement, IElement parent, IRule rule) + { + Type elementType = srgsElement.GetType(); + IElement child = null; + IRule parentRule = parent as IRule; + IItem parentItem = parent as IItem; + + if (elementType == typeof(SrgsRuleRef)) + { + child = ParseRuleRef((SrgsRuleRef)srgsElement, parent); + } + else if (elementType == typeof(SrgsOneOf)) + { + child = ParseOneOf((SrgsOneOf)srgsElement, parent, rule); + } + else if (elementType == typeof(SrgsItem)) + { + child = ParseItem((SrgsItem)srgsElement, parent, rule); + } + else if (elementType == typeof(SrgsToken)) + { + child = ParseToken((SrgsToken)srgsElement, parent); + } + else if (elementType == typeof(SrgsNameValueTag)) + { + child = ParseNameValueTag((SrgsNameValueTag)srgsElement, parent); + } + else if (elementType == typeof(SrgsSemanticInterpretationTag)) + { + child = ParseSemanticTag((SrgsSemanticInterpretationTag)srgsElement, parent); + } + else if (elementType == typeof(SrgsSubset)) + { + child = ParseSubset((SrgsSubset)srgsElement, parent); + } + else if (elementType == typeof(SrgsText)) + { + SrgsText srgsText = (SrgsText)srgsElement; + string content = srgsText.Text; + + // Create the SrgsElement for the text + IElementText textChild = _parser.CreateText(parent, content); + + // Split it in pieces + ParseText(parent, content, null, null, -1f); + + if (parentRule != null) + { + _parser.AddElement(parentRule, textChild); + } + else + { + if (parentItem != null) + { + _parser.AddElement(parentItem, textChild); + } + else + { + XmlParser.ThrowSrgsException(SRID.InvalidElement); + } + } + } + else + { + System.Diagnostics.Debug.Assert(false, "Unsupported Srgs element"); + XmlParser.ThrowSrgsException(SRID.InvalidElement); + } + + // if the parent is a one of, then the children must be an Item + IOneOf parentOneOf = parent as IOneOf; + if (parentOneOf != null) + { + IItem childItem = child as IItem; + if (childItem != null) + { + _parser.AddItem(parentOneOf, childItem); + } + else + { + XmlParser.ThrowSrgsException(SRID.InvalidElement); + } + } + else + { + if (parentRule != null) + { + _parser.AddElement(parentRule, child); + } + else + { + if (parentItem != null) + { + _parser.AddElement(parentItem, child); + } + else + { + XmlParser.ThrowSrgsException(SRID.InvalidElement); + } + } + } + } + + private IEnumerable GetSortedTagElements(Collection elements) + { + if (_grammar.TagFormat == SrgsTagFormat.KeyValuePairs) + { + List list = new(); + foreach (SrgsElement element in elements) + { + if (!(element is SrgsNameValueTag)) + { + list.Add(element); + } + } + foreach (SrgsElement element in elements) + { + if ((element is SrgsNameValueTag)) + { + list.Add(element); + } + } + return list; + } + else + { + // Semantic Interpretation, the order for the tag element does not matter + return elements; + } + } + + #endregion + + #region Private Fields + + private SrgsGrammar _grammar; + + private IElementFactory _parser; + + #endregion + } +} diff --git a/src/libraries/System.Speech/src/Internal/SrgsParser/XmlParser.cs b/src/libraries/System.Speech/src/Internal/SrgsParser/XmlParser.cs new file mode 100644 index 00000000000000..7374226095f894 --- /dev/null +++ b/src/libraries/System.Speech/src/Internal/SrgsParser/XmlParser.cs @@ -0,0 +1,1922 @@ +// 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.Globalization; +using System.Speech.Recognition.SrgsGrammar; +using System.Text; +using System.Xml; + +#pragma warning disable 56524 // The _reader and _xmlReader are not created in this module and should not be disposed + +// Remove all the check for null or empty warnings + +namespace System.Speech.Internal.SrgsParser +{ + internal class XmlParser : ISrgsParser + { + #region Constructors + + internal XmlParser(XmlReader reader, Uri uri) + { + _reader = reader; + _xmlTextReader = reader as XmlTextReader; + + // Try to guess the Uri + if (uri == null) + { + // Keep a reference to the filename and XmlTextReader if it is one. + if (_xmlTextReader != null && _xmlTextReader.BaseURI.Length > 0) + { + try + { + uri = new Uri(_xmlTextReader.BaseURI); + } +#pragma warning disable 56502 // Remove the empty catch statements warnings + catch (UriFormatException) + { + } +#pragma warning restore 56502 + } + } + + // Saves the path to the file and the file name + if (uri != null) + { + // Saves the full path to the file + _filename = !uri.IsAbsoluteUri || !uri.IsFile ? uri.OriginalString : uri.LocalPath; + + // Saves the filename without the path + int iPosSlash = _filename.LastIndexOfAny(s_slashBackSlash); + _shortFilename = iPosSlash >= 0 ? _filename.Substring(iPosSlash + 1) : _filename; + } + } + + #endregion + + #region Internal Methods + + // Initializes the object from a stream containing SRGS in XML + public void Parse() + { + try + { + bool isGrammarElementFound = false; + + while (_reader.Read()) + { + // Ignore XmlDeclaration, ProcessingInstruction, Comment, DocumentType, Entity, Notation. + if (_reader.NodeType == XmlNodeType.Element && _reader.LocalName == "grammar") + { + if (_reader.NamespaceURI != srgsNamespace) + { + ThrowSrgsException(SRID.InvalidSrgsNamespace); + } + + if (isGrammarElementFound) + { + ThrowSrgsException(SRID.GrammarDefTwice); + } + else + { + ParseGrammar(_reader, _parser.Grammar); + isGrammarElementFound = true; + } + } + } + + if (!isGrammarElementFound) + { + ThrowSrgsException(SRID.InvalidSrgs); + } + } + catch (XmlException eXml) + { + _parser.RemoveAllRules(); + ThrowSrgsExceptionWithPosition(_filename, _reader, SR.Get(SRID.InvalidXmlFormat), eXml); + } + catch (FormatException e) + { + // Adds a placeholder for the rule. + // Once all the rules and scripts are read, the placeholder will be replaced with the proper rule. + _parser.RemoveAllRules(); + ThrowSrgsExceptionWithPosition(_filename, _reader, e.Message, e.InnerException); + } + catch + { + // clear all the rules + _parser.RemoveAllRules(); + throw; + } + } + + /// + /// Break the string into individual tokens and ParseToken() each individual token. + /// + /// Token string is a sequence of 0 or more white space delimited tokens. + /// Tokens may also be delimited by double quotes. In these cases, the double + /// quotes token must be surrounded by white space or string boundary. + /// + internal static void ParseText(IElement parent, string sChars, string pronunciation, string display, float reqConfidence, CreateTokenCallback createTokens) + { + sChars = sChars.Trim(Helpers._achTrimChars); + + char[] achToken = sChars.ToCharArray(); + int iTokenEnd = 0; + int cChars = sChars.Length; + + for (int i = 0; i < achToken.Length; i = iTokenEnd + 1) + { + if (achToken[i] == ' ') // Skip white spaces + { + iTokenEnd = i; + continue; + } + + // Find the next token + if (achToken[i] == '"') + { + // Quoted string. Find end of quoted string. + iTokenEnd = ++i; + while ((iTokenEnd < cChars) && (achToken[iTokenEnd] != '"')) + { + iTokenEnd++; + } + + if (iTokenEnd >= cChars || achToken[iTokenEnd] != '"') + { + // Cannot find matching double quote. + // "Invalid double-quoted string." + XmlParser.ThrowSrgsException(SRID.InvalidQuotedString); + } + + if (iTokenEnd + 1 != cChars && achToken[iTokenEnd + 1] != ' ') + { + // Quoted token not surrounded by whitespace."); + // "Invalid double-quoted string." + XmlParser.ThrowSrgsException(SRID.InvalidQuotedString); + } + } + else + { + // Regular token. Find next white space character or end of string + iTokenEnd = i + 1; + while ((iTokenEnd < cChars) && achToken[iTokenEnd] != ' ') + { + iTokenEnd++; + } + } + + string sToken = sChars.Substring(i, iTokenEnd - i); + if (sToken.IndexOf('"') != -1) + { + // "The token string is not allowed to contain double quote character." + XmlParser.ThrowSrgsException(SRID.InvalidTokenString); + } + + // Parse the token. + if (createTokens != null) + { + createTokens(parent, sToken, pronunciation, display, reqConfidence); + } + } + } + + /// + /// Throws an Exception with the error specified by the resource ID. + /// Add the line and column number if the XmlReader is a TextReader + /// + internal static void ThrowSrgsException(SRID id, params object[] args) + { + throw new FormatException(SR.Get(id, args)); + } + + /// + /// Throws an Exception with the error specified by the resource ID. + /// Add the line and column number if the XmlReader is a TextReader + /// + internal static void ThrowSrgsExceptionWithPosition(string filename, XmlReader xmlReader, string sError, Exception innerException) + { + // Add the line and column number if the XmlReader is a XmlTextReader + XmlTextReader xmlTextReader = xmlReader as XmlTextReader; + if (xmlTextReader != null) + { + string sLine = SR.Get(SRID.Line); + string sPosition = SR.Get(SRID.Position); + int line = xmlTextReader.LineNumber; + int position = xmlTextReader.LinePosition; + if (filename == null) + { + sError += string.Format(CultureInfo.InvariantCulture, " [{0}={1}, {2}={3}]", sLine, line, sPosition, position); + } + else + { + sError = string.Format(CultureInfo.InvariantCulture, "{0}({1},{2}): error : {3}", filename, line, position, sError); + } + } + throw new FormatException(sError, innerException); + } + + #endregion + + #region Internal Methods + + // Implementation of the internal interface ISrgsParser + public IElementFactory ElementFactory + { + set + { + _parser = value; + } + } + + #endregion + + #region Internal fields + + internal const string emptyNamespace = ""; + + internal const string xmlNamespace = "http://www.w3.org/XML/1998/namespace"; + + internal const string srgsNamespace = "http://www.w3.org/2001/06/grammar"; + + internal const string sapiNamespace = "http://schemas.microsoft.com/Speech/2002/06/SRGSExtensions"; + + #endregion + + #region Private Type + + // Must be a class to be used with generics + [Serializable] + internal class ForwardReference + { + internal ForwardReference(string name, string value) + { + _name = name; + _value = value; + } + + internal string _name; + internal string _value; + } + #endregion + + #region Private Methods + + // The perf gain using .Lengh == 0 other readability is not worth it fixing this FxCop issue + private void ParseGrammar(XmlReader reader, IGrammar grammar) + { + string sAlphabet = null; + string sLanguage = null; + string sNamespace = null; + string sVersion = null; + GrammarType grammarType = GrammarType.VoiceGrammar; + + // Process attributes. + while (reader.MoveToNextAttribute()) + { + bool isInvalidAttribute = false; + + switch (reader.NamespaceURI) + { + case emptyNamespace: + switch (reader.LocalName) + { + case "root": + if (grammar.Root == null) + { + grammar.Root = reader.Value; + } + else + { + ThrowSrgsException(SRID.RootRuleAlreadyDefined); + } + break; + + case "version": + CheckForDuplicates(ref sVersion, reader); + if (sVersion != "1.0") + { + ThrowSrgsException(SRID.InvalidVersion); + } + + break; + + case "tag-format": + switch (reader.Value) + { + case "semantics/1.0": + grammar.TagFormat = SrgsTagFormat.W3cV1; + _hasTagFormat = true; + break; + + case "semantics-ms/1.0": + grammar.TagFormat = SrgsTagFormat.MssV1; + _hasTagFormat = true; + break; + + case "properties-ms/1.0": + grammar.TagFormat = SrgsTagFormat.KeyValuePairs; + _hasTagFormat = true; + break; + + case "": + break; + + default: + ThrowSrgsException(SRID.InvalidTagFormat); + break; + } + break; + + case "mode": + switch (reader.Value) + { + case "voice": + grammar.Mode = GrammarType.VoiceGrammar; + break; + + case "dtmf": + grammarType = grammar.Mode = GrammarType.DtmfGrammar; + break; + + default: + ThrowSrgsException(SRID.InvalidGrammarMode); + break; + } + break; + + default: + isInvalidAttribute = true; + break; + } + break; + + case xmlNamespace: + switch (reader.LocalName) + { + case "lang": + string language = reader.Value; + try + { + grammar.Culture = _langId = new CultureInfo(language); + } + catch (ArgumentException) + { + // Unknown Culture info, fall back to the base culture. + int pos = reader.Value.IndexOf("-", StringComparison.Ordinal); + if (pos > 0) + { + grammar.Culture = _langId = new CultureInfo(reader.Value.Substring(0, pos)); + } + else + { + throw; + } + } + break; + + case "base": + grammar.XmlBase = new Uri(reader.Value); + break; + } + break; + + case sapiNamespace: + switch (reader.LocalName) + { + case "alphabet": + CheckForDuplicates(ref sAlphabet, reader); + switch (sAlphabet) + { + case "ipa": + grammar.PhoneticAlphabet = AlphabetType.Ipa; + break; + + case "sapi": + case "x-sapi": + case "x-microsoft-sapi": + grammar.PhoneticAlphabet = AlphabetType.Sapi; + break; + + case "ups": + case "x-ups": + case "x-microsoft-ups": + grammar.PhoneticAlphabet = AlphabetType.Ups; + break; + + default: + ThrowSrgsException(SRID.UnsupportedPhoneticAlphabet, reader.Value); + break; + } + break; + + case "language": + CheckForDuplicates(ref sLanguage, reader); + if (sLanguage == "C#" || sLanguage == "VB.Net") + { + grammar.Language = sLanguage; + } + else + { + ThrowSrgsException(SRID.UnsupportedLanguage, reader.Value); + } + break; + + case "namespace": + CheckForDuplicates(ref sNamespace, reader); + if (string.IsNullOrEmpty(sNamespace)) + { + ThrowSrgsException(SRID.NoName1, "namespace"); + } + grammar.Namespace = sNamespace; + break; + + case "codebehind": + if (reader.Value.Length == 0) + { + ThrowSrgsException(SRID.NoName1, "codebehind"); + } + grammar.CodeBehind.Add(reader.Value); + break; + + case "debug": + bool f; + if (bool.TryParse(reader.Value, out f)) + { + grammar.Debug = f; + } + break; + default: + isInvalidAttribute = true; + break; + } + break; + } + if (isInvalidAttribute) + { + ThrowSrgsException(SRID.InvalidGrammarAttribute, reader.Name); + } + } + + // The version attribute is required for the grammar element + if (sVersion == null) + { + ThrowSrgsException(SRID.MissingRequiredAttribute, "version", "grammar"); + } + + // The langId is require for voice grammars + if (_langId == null) + { + if (grammarType == GrammarType.VoiceGrammar) + { + ThrowSrgsException(SRID.MissingRequiredAttribute, "xml:lang", "grammar"); + } + else + { + _langId = CultureInfo.CurrentUICulture; + } + } + + // Process child elements. + ProcessRulesAndScriptsNodes(reader, grammar); + + // Validate all the scripts elements + ValidateScripts(); + + // Add all the scripts to the rules + foreach (ForwardReference script in _scripts) + { + _parser.AddScript(grammar, script._name, script._value); + } + // Finish all initialization - should check for the Root and the all + // rules are defined + grammar.PostParse(null); + } + + // The perf gain using .Lengh == 0 other readability is not worth it fixing this FxCop issue + private IRule ParseRule(IGrammar grammar, XmlReader reader) + { + string id = null; + string scope = null; + string dynamic = null; + RulePublic publicRule = RulePublic.NotSet; + RuleDynamic ruleDynamic = RuleDynamic.NotSet; + + string sBaseClass = null; + string sInit = null; + string sParse = null; + string sError = null; + string sRecognition = null; + + while (reader.MoveToNextAttribute()) + { + bool isInvalidAttribute = false; + + switch (reader.NamespaceURI) + { + case emptyNamespace: + switch (reader.LocalName) + { + case "id": + CheckForDuplicates(ref id, reader); + break; + + case "scope": + CheckForDuplicates(ref scope, reader); + switch (scope) + { + case "private": + publicRule = RulePublic.False; + break; + + case "public": + publicRule = RulePublic.True; + break; + + default: + ThrowSrgsException(SRID.InvalidRuleScope); + break; + } + break; + + default: + isInvalidAttribute = true; + break; + } + break; + + case sapiNamespace: + switch (reader.LocalName) + { + case "dynamic": + CheckForDuplicates(ref dynamic, reader); + switch (dynamic) + { + case "true": + ruleDynamic = RuleDynamic.True; + break; + + case "false": + ruleDynamic = RuleDynamic.False; + break; + + default: + ThrowSrgsException(SRID.InvalidDynamicSetting); + break; + } + break; + + case "baseclass": + CheckForDuplicates(ref sBaseClass, reader); + if (string.IsNullOrEmpty(sBaseClass)) + { + ThrowSrgsException(SRID.NoName1, "baseclass"); + } + break; + + case "onInit": + CheckForDuplicates(ref sInit, reader); + sInit = reader.Value; + break; + + case "onParse": + CheckForDuplicates(ref sParse, reader); + sParse = reader.Value; + break; + + case "onError": + CheckForDuplicates(ref sError, reader); + sError = reader.Value; + break; + + case "onRecognition": + CheckForDuplicates(ref sRecognition, reader); + break; + default: + isInvalidAttribute = true; + break; + } + break; + } + if (isInvalidAttribute) + { + ThrowSrgsException(SRID.InvalidRuleAttribute, reader.Name); + } + } + + if (string.IsNullOrEmpty(id)) + { + ThrowSrgsException(SRID.NoRuleId); + } + + if (sInit != null && publicRule != RulePublic.True) + { + XmlParser.ThrowSrgsException(SRID.OnInitOnPublicRule, "OnInit", id); + } + + if (sRecognition != null && publicRule != RulePublic.True) + { + XmlParser.ThrowSrgsException(SRID.OnInitOnPublicRule, "OnRecognition", id); + } + + ValidateRuleId(id); + + bool hasScript = sInit != null || sParse != null || sError != null || sRecognition != null; + IRule rule = grammar.CreateRule(id, publicRule, ruleDynamic, hasScript); + + if (!string.IsNullOrEmpty(sInit)) + { + rule.CreateScript(grammar, id, sInit, RuleMethodScript.onInit); + } + + if (!string.IsNullOrEmpty(sParse)) + { + rule.CreateScript(grammar, id, sParse, RuleMethodScript.onParse); + } + + if (!string.IsNullOrEmpty(sError)) + { + rule.CreateScript(grammar, id, sError, RuleMethodScript.onError); + } + + if (!string.IsNullOrEmpty(sRecognition)) + { + rule.CreateScript(grammar, id, sRecognition, RuleMethodScript.onRecognition); + } + + rule.BaseClass = sBaseClass; + _rules.Add(id); + + if (!ProcessChildNodes(reader, rule, rule, "rule")) + { + if (ruleDynamic != RuleDynamic.True) + { + ThrowSrgsException(SRID.InvalidEmptyRule, "rule", id); + } + } + return rule; + } + + // The perf gain using .Lengh == 0 other readability is not worth it fixing this FxCop issue + private IRuleRef ParseRuleRef(IElement parent, XmlReader reader) + { + IRuleRef ruleRef = null; + + string sAlias = null; + string sParams = null; + string uri = null; + + while (reader.MoveToNextAttribute()) + { + bool isInvalidAttribute = false; + + switch (reader.NamespaceURI) + { + case emptyNamespace: + switch (reader.LocalName) + { + case "uri": + // Check that the uri pointed to in the ruleref does not point this file + // in srgs.xml: ... = 0) + { + ThrowSrgsException(SRID.InvalidTokenString); + } + + return _parser.CreateToken(parent, content, sPronunciation, sDisplay, reqConfidence); + } + + /// + /// Break the string into individual tokens and ParseToken() each individual token. + /// + /// Token string is a sequence of 0 or more white space delimited tokens. + /// Tokens may also be delimited by double quotes. In these cases, the double + /// quotes token must be surrounded by white space or string boundary. + /// + private void ParseText(IElement parent, string sChars, string pronunciation, string display, float reqConfidence) + { + System.Diagnostics.Debug.Assert((parent != null) && (!string.IsNullOrEmpty(sChars))); + + ParseText(parent, sChars, pronunciation, display, reqConfidence, new CreateTokenCallback(_parser.CreateToken)); + } + + private IElement ParseTag(IElement parent, XmlReader reader) + { + string content = GetTagContent(parent, reader); + + //Return an empty tag if the content is empty + if (string.IsNullOrEmpty(content)) + { + return _parser.CreateSemanticTag(parent); + } + + if (_parser.Grammar.TagFormat != SrgsTagFormat.KeyValuePairs) + { + ISemanticTag semanticTag = _parser.CreateSemanticTag(parent); + + semanticTag.Content(parent, content, 0); + return semanticTag; + } + + System.Diagnostics.Debug.Assert(_parser.Grammar.TagFormat == SrgsTagFormat.KeyValuePairs); + + IPropertyTag propertyTag = _parser.CreatePropertyTag(parent); ; + string name; + object value; + ParsePropertyTag(content, out name, out value); + propertyTag.NameValue(parent, name, value); + return propertyTag; + } + + private string GetTagContent(IElement parent, XmlReader reader) + { + // A tag format must be specified in the grammar header + if (!_hasTagFormat) + { + ThrowSrgsException(SRID.MissingTagFormat); + } + + while (reader.MoveToNextAttribute()) + { + bool isInvalidAttribute = false; + + switch (reader.NamespaceURI) + { + case emptyNamespace: + case sapiNamespace: + isInvalidAttribute = true; + break; + } + if (isInvalidAttribute) + { + ThrowSrgsException(SRID.InvalidTagAttribute, reader.Name); + } + } + + return GetStringContent(reader).Trim(Helpers._achTrimChars); + } + + /// + /// Parse the lexicon Element + /// + /// Attributes: + /// uri: required + /// type: optional + /// + private static void ParseLexicon(XmlReader reader) + { + bool isInvalidAttribute = false; + bool fFoundUri = false; + + while (reader.MoveToNextAttribute()) + { + switch (reader.LocalName) + { + case "uri": + fFoundUri = true; + break; + + case "type": + break; + + default: + isInvalidAttribute = true; + break; + } + + if (isInvalidAttribute) + { + ThrowSrgsException(SRID.InvalidLexiconAttribute, reader.Name); + } + } + + if (!fFoundUri) + { + ThrowSrgsException(SRID.MissingRequiredAttribute, "uri", "lexicon"); + } + } + + /// + /// Parse the Meta Element + /// + /// Attributes: + /// name and http-equiv: one or the other but not both + /// content: required + /// + private static void ParseMeta(XmlReader reader) + { + bool fFoundContent = false; + bool fFoundNameOrEquiv = false; + bool isInvalidAttribute = false; + + while (reader.MoveToNextAttribute()) + { + switch (reader.LocalName) + { + case "name": + case "http-equiv": + if (fFoundNameOrEquiv) + { + ThrowSrgsException(SRID.MetaNameHTTPEquiv); + } + fFoundNameOrEquiv = true; + break; + + case "content": + isInvalidAttribute = fFoundContent; + fFoundContent = true; + break; + + default: + isInvalidAttribute = true; + break; + } + + if (isInvalidAttribute) + { + ThrowSrgsException(SRID.InvalidMetaAttribute, reader.Name); + } + } + + if (!fFoundContent) + { + ThrowSrgsException(SRID.MissingRequiredAttribute, "content", "meta"); + } + if (!fFoundNameOrEquiv) + { + ThrowSrgsException(SRID.MissingRequiredAttribute, "name or http-equiv", "meta"); + } + } + + private void ParseScript(XmlReader reader, IGrammar grammar) + { + int line = _filename != null ? _xmlTextReader.LineNumber : -1; + string sRule = null; + + while (reader.MoveToNextAttribute()) + { + switch (reader.NamespaceURI) + { + case emptyNamespace: + ThrowSrgsException(SRID.InvalidScriptAttribute); + break; + + case sapiNamespace: + switch (reader.LocalName) + { + case "rule": + if (string.IsNullOrEmpty(sRule)) + { + sRule = reader.Value; + } + else + { + ThrowSrgsException(SRID.RuleAttributeDefinedMultipeTimes); + } + break; + + default: + ThrowSrgsException(SRID.InvalidScriptAttribute); + break; + } + break; + } + } + // if no rule or method defined - add the content to the generic _scrip + if (string.IsNullOrEmpty(sRule)) + { + _parser.AddScript(grammar, GetStringContent(reader), _filename, line); + } + else + { + // Adds a placeholder for the rule. + // Once all the rules and scripts are read, the placeholder will be replaced with the proper rule. + _scripts.Add(new ForwardReference(sRule, _parser.AddScript(grammar, sRule, GetStringContent(reader), _filename, line))); + } + } + + private static void ParseAssemblyReference(XmlReader reader, IGrammar grammar) + { + while (reader.MoveToNextAttribute()) + { + switch (reader.NamespaceURI) + { + case emptyNamespace: + ThrowSrgsException(SRID.InvalidScriptAttribute); + break; + + case sapiNamespace: + switch (reader.LocalName) + { + case "assembly": + grammar.AssemblyReferences.Add(reader.Value); + break; + + default: + ThrowSrgsException(SRID.InvalidAssemblyReferenceAttribute); + break; + } + break; + } + } + } + + private static void ParseImportNamespace(XmlReader reader, IGrammar grammar) + { + while (reader.MoveToNextAttribute()) + { + switch (reader.NamespaceURI) + { + case emptyNamespace: + ThrowSrgsException(SRID.InvalidScriptAttribute); + break; + + case sapiNamespace: + switch (reader.LocalName) + { + case "namespace": + grammar.ImportNamespaces.Add(reader.Value); + break; + + default: + ThrowSrgsException(SRID.InvalidImportNamespaceAttribute); + break; + } + break; + } + } + } + + private bool ProcessChildNodes(XmlReader reader, IElement parent, IRule rule, string parentName) + { + bool fFirstElement = true; + + // Create a list of name value tags for this scope + List tags = null; + + reader.MoveToElement(); // Move to containing parent of attributes + if (!reader.IsEmptyElement) + { + reader.Read(); // Move to first child parent + while (reader.NodeType != XmlNodeType.EndElement) // Process each child parent while not at end parent + { + bool isInvalidNode = false; + + if (reader.NodeType == XmlNodeType.Element) + { + // Null if no children are allowed + if (parent == null) + { + ThrowSrgsException(SRID.InvalidNotEmptyElement, parentName); + } + + IElement child = null; + switch (reader.NamespaceURI) + { + case srgsNamespace: + + switch (reader.LocalName) + { + case "example": + if (!(parent is IRule) || !fFirstElement) + { + ThrowSrgsException(SRID.InvalidExampleOrdering); + } + else + { + reader.Skip(); + continue; + } + + break; + + case "ruleref": + child = ParseRuleRef(parent, reader); + break; + + case "one-of": + child = ParseOneOf(parent, rule, reader); + break; + + case "item": + child = ParseItem(parent, rule, reader); + break; + + case "token": + child = ParseToken(parent, reader); + break; + + case "tag": + child = ParseTag(parent, reader); + IPropertyTag tag = child as IPropertyTag; + if (tag != null) + { + // The tag list is delayed as it might not be necessary + if (tags == null) + { + tags = new List(); + } + tags.Add(tag); + } + break; + + case "rule": + default: + isInvalidNode = true; + break; + } + break; + + case sapiNamespace: + switch (reader.LocalName) + { + case "subset": + if ((parent is IRule) || (parent is IItem)) + { + child = ParseSubset(parent, reader); + } + else + { + isInvalidNode = true; + } + break; + + default: + isInvalidNode = true; + break; + } + break; + + default: + reader.Skip(); // Skip over parents in unknown namespaces + break; + } + isInvalidNode = ParseChildNodeElement(parent, isInvalidNode, child); + fFirstElement = false; + } + else if (reader.NodeType == XmlNodeType.Text || reader.NodeType == XmlNodeType.CDATA) + { + // Null if no children are allowed + if (parent == null) + { + ThrowSrgsException(SRID.InvalidNotEmptyElement, parentName); + } + + isInvalidNode = ParseChildNodeText(reader, parent); + fFirstElement = false; + } + else + { + reader.Skip(); // Skip over non-parent/text node types + } + + if (isInvalidNode) + { + ThrowSrgsException(SRID.InvalidElement, reader.Name); + } + } + } + + reader.Read(); // Move to next sibling + + // Generate the tags for this scope + if (tags != null) + { + foreach (IPropertyTag tag in tags) + { + tag.PostParse(parent); + } + } + return !fFirstElement; + } + + private bool ParseChildNodeText(XmlReader reader, IElement parent) + { + bool isInvalidNode = false; + string content = reader.Value; + + // Create the SrgsElement for the text + IElementText srgsText = _parser.CreateText(parent, content); + + // Split it in pieces + ParseText(parent, content, null, null, -1f); + + // if the parent is a one of, then the children must be an Item + if (parent is IOneOf) + { + isInvalidNode = true; + } + else + { + IRule parentRule = parent as IRule; + if (parentRule != null) + { + _parser.AddElement(parentRule, srgsText); + } + else + { + IItem parentItem = parent as IItem; + if (parentItem != null) + { + _parser.AddElement(parentItem, srgsText); + } + else + { + isInvalidNode = true; + } + } + } + + reader.Read(); + return isInvalidNode; + } + + private bool ParseChildNodeElement(IElement parent, bool isInvalidNode, IElement child) + { + // The child parent has not been processed yet + if (child != null) + { + // if the parent is a one of, then the children must be an Item + IOneOf parentOneOf = parent as IOneOf; + if (parentOneOf != null) + { + IItem childItem = child as IItem; + if (childItem != null) + { + _parser.AddItem(parentOneOf, childItem); + } + else + { + isInvalidNode = true; + } + } + else + { + IRule parentRule = parent as IRule; + if (parentRule != null) + { + _parser.AddElement(parentRule, child); + } + else + { + IItem parentItem = parent as IItem; + if (parentItem != null) + { + _parser.AddElement(parentItem, child); + } + else + { + isInvalidNode = true; + } + } + } + } + + return isInvalidNode; + } + + private void ProcessRulesAndScriptsNodes(XmlReader reader, IGrammar grammar) + { + bool fProcessedRules = false; + + // Move to containing element of attributes + reader.MoveToElement(); + if (!reader.IsEmptyElement) + { + // Move to first child element + reader.Read(); + + // Process each child element while not at end element + while (reader.NodeType != XmlNodeType.EndElement) + { + bool isInvalidNode = false; + + if (reader.NodeType == XmlNodeType.Element) + { + switch (reader.NamespaceURI) + { + case srgsNamespace: + switch (reader.LocalName) + { + case "lexicon": + if (fProcessedRules) + { + ThrowSrgsException(SRID.InvalidGrammarOrdering); + } + ParseLexicon(reader); + break; + + case "meta": + if (fProcessedRules) + { + ThrowSrgsException(SRID.InvalidGrammarOrdering); + } + ParseMeta(reader); + break; + + case "metadata": + if (fProcessedRules) + { + ThrowSrgsException(SRID.InvalidGrammarOrdering); + } + reader.Skip(); + break; + + case "rule": + IRule rule = ParseRule(grammar, reader); + rule.PostParse(grammar); + fProcessedRules = true; + break; + + case "tag": + if (fProcessedRules || _hasTagFormat && grammar.TagFormat != SrgsTagFormat.W3cV1) + { + ThrowSrgsException(SRID.InvalidGrammarOrdering); + } + grammar.GlobalTags.Add(GetTagContent(grammar, reader)); + break; + + default: + isInvalidNode = true; + break; + } + break; + + case sapiNamespace: + switch (reader.LocalName) + { + case "script": + ParseScript(reader, grammar); + fProcessedRules = true; + break; + + case "assemblyReference": + ParseAssemblyReference(reader, grammar); + fProcessedRules = true; + break; + + case "importNamespace": + ParseImportNamespace(reader, grammar); + fProcessedRules = true; + break; + default: + isInvalidNode = true; + break; + } + break; + + default: + // Skip over elements in unknown namespaces + reader.Skip(); + break; + } + } + else + { + if (reader.NodeType == XmlNodeType.Text) + { + ThrowSrgsException(SRID.InvalidElement, "text"); + } + // Skip over non-element/text node types + reader.Skip(); + } + + if (isInvalidNode) + { + ThrowSrgsException(SRID.InvalidElement, reader.Name); + } + } + } + + // Move to next sibling + reader.Read(); + } + + private static string GetStringContent(XmlReader reader) + { + StringBuilder sb = new(); + + reader.MoveToElement(); // Move to containing element of attributes + if (!reader.IsEmptyElement) + { + reader.Read(); // Move to first child element + while (reader.NodeType != XmlNodeType.EndElement) // Process each child element while not at end element + { + sb.Append(reader.ReadString()); + + bool isInvalidNode = false; + + if (reader.NodeType == XmlNodeType.Element) + { + switch (reader.NamespaceURI) + { + case srgsNamespace: + case sapiNamespace: + isInvalidNode = true; + break; + + default: + reader.Skip(); // Skip over elements in unknown namespaces + break; + } + } + else if (reader.NodeType != XmlNodeType.EndElement) + { + reader.Skip(); // Skip over non-end element node types + } + + if (isInvalidNode) + { + ThrowSrgsException(SRID.InvalidElement, reader.Name); + } + } + } + + reader.Read(); // Move to next sibling + return sb.ToString(); + } + private static void ParsePropertyTag(string sTag, out string name, out object value) + { + // Default value + name = null; + value = string.Empty; + + // Name= pszValue = null vValue = VT_EMPTY + // Name="string" pszValue = "string" vValue = VT_EMPTY + // Name=true pszValue = null vValue = VT_BOOL + // Name=123 pszValue = null vValue = VT_I4 + // Name=3.14 pszValue = null vValue = VT_R8 + int iEqual = sTag.IndexOf('='); + + if (iEqual >= 0) + { + // Set property name + name = sTag.Substring(0, iEqual).Trim(Helpers._achTrimChars); + iEqual++; + } + else + { + iEqual = 0; + } + + // Set property value + int cLenProperty = sTag.Length; + + if (iEqual < cLenProperty) + { + if (sTag[iEqual] == '"') + { + // Name="string" + iEqual++; + + int iEndQuote = sTag.IndexOf('"', iEqual + 1); + + if (iEndQuote + 1 != cLenProperty) + { + // Invalid string value + XmlParser.ThrowSrgsException(SRID.IncorrectAttributeValue, name, sTag.Substring(iEqual)); + } + + value = sTag.Substring(iEqual, iEndQuote - iEqual); + } + else + { + string sValue = sTag.Substring(iEqual); + int iValue; + + if (int.TryParse(sValue, out iValue)) + { + // propInfo.pszValue = null + // Name=123 + // propInfo.vValue = VT_I4 + value = iValue; + } + else + { + double flValue; + + if (double.TryParse(sValue, out flValue)) + { + // propInfo.pszValue = null + // propInfo.vValue = VT_R8 + value = flValue; + } + else + { + bool fValue; + + if (bool.TryParse(sValue, out fValue)) + { + // Name=true + // propInfo.pszValue = null + // propInfo.vValue = VT_BOOL + value = fValue; + } + else + { + XmlParser.ThrowSrgsException(SRID.InvalidNameValueProperty, name, sValue); + } + } + } + } + } + } + + /// + /// Convert integer range string to MinValue and MaxValue. + /// For n- format, MaxValue = Int32.MaxValue + /// Valid formats: n|n-|n-m n,m integers + /// integer = [whitespace] [+] [0[{x|X}]] [digits] + /// + private static void SetRepeatValues(string repeat, out int minRepeat, out int maxRepeat) + { + minRepeat = maxRepeat = 1; + if (!string.IsNullOrEmpty(repeat)) + { + int sep = repeat.IndexOf("-", StringComparison.Ordinal); + + if (sep < 0) + { + int minmax = Convert.ToInt32(repeat, CultureInfo.InvariantCulture); + + // Limit the range of valid values + if (minmax < 0 || minmax > 255) + { + XmlParser.ThrowSrgsException(SRID.MinMaxOutOfRange, minmax, minmax); + } + minRepeat = maxRepeat = minmax; + } + else if (0 < sep) + { + minRepeat = Convert.ToInt32(repeat.Substring(0, sep), CultureInfo.InvariantCulture); + if (sep < (repeat.Length - 1)) + { + maxRepeat = Convert.ToInt32(repeat.Substring(sep + 1), CultureInfo.InvariantCulture); + } + else + { + maxRepeat = int.MaxValue; + } + // Limit the range of valid values + if (minRepeat < 0 || minRepeat > 255 || (maxRepeat != int.MaxValue && (maxRepeat < 0 || maxRepeat > 255))) + { + XmlParser.ThrowSrgsException(SRID.MinMaxOutOfRange, minRepeat, maxRepeat); + } + + // Max be greater or equal to min + if (minRepeat > maxRepeat) + { + throw new ArgumentException(SR.Get(SRID.MinGreaterThanMax)); + } + } + else + { + ThrowSrgsException(SRID.InvalidItemRepeatAttribute, repeat); + } + } + else + { + ThrowSrgsException(SRID.InvalidItemAttribute2); + } + } + + private static void CheckForDuplicates(ref string dest, XmlReader reader) + { + if (!string.IsNullOrEmpty(dest)) + { + StringBuilder attribute = new(reader.LocalName); + if (reader.NamespaceURI.Length > 0) + { + attribute.Append(reader.NamespaceURI); + attribute.Append(':'); + } + XmlParser.ThrowSrgsException(SRID.InvalidAttributeDefinedTwice, reader.Value, attribute); + } + dest = reader.Value; + } + + // Throws exception if the specified Rule does not have a valid Id. + internal static void ValidateRuleId(string id) + { + Helpers.ThrowIfEmptyOrNull(id, nameof(id)); + + if (!XmlReader.IsName(id) || (id == "NULL") || (id == "VOID") || (id == "GARBAGE") || (id.IndexOfAny(s_invalidRuleIdChars) != -1)) + { + XmlParser.ThrowSrgsException(SRID.InvalidRuleId, id); + } + } + + private void ValidateRulerefNotPointingToSelf(string uri) + { + // Check that the uri pointed to in the ruleref does not point this file + // in srgs.xml: ... + if (_filename != null) + { + if (uri.IndexOf(_shortFilename, StringComparison.Ordinal) == 0 && (uri.Length > _shortFilename.Length && uri[_shortFilename.Length] == '#' || uri.Length == _shortFilename.Length)) + { + ThrowSrgsException(SRID.InvalidRuleRefSelf); + } + } + } + + private void ValidateScripts() + { + // Check that the rule and methods are defined for a script + foreach (ForwardReference script in _scripts) + { + if (!_rules.Contains(script._name)) + { + ThrowSrgsException(SRID.InvalidScriptDefinition, script._name); + } + } + // Validate for unique rule names + List ruleNames = new(); + + foreach (string rule in _rules) + { + if (ruleNames.Contains(rule)) + { + XmlParser.ThrowSrgsException(SRID.RuleAttributeDefinedMultipeTimes, rule); + } + + ruleNames.Add(rule); + } + } + + #endregion + + #region Private Fields + + private IElementFactory _parser; + + // Avoid to do a cast many times + private XmlReader _reader; + + // Avoid to do a cast many times + private XmlTextReader _xmlTextReader; + + // Save the filename + private string _filename; + + // Save the filename without the path + private string _shortFilename; + + // Language Id for this grammar + private CultureInfo _langId; + + // Has the Grammar element a FormatTag + private bool _hasTagFormat; + + // All defined rules + private List _rules = new(); + + private List _scripts = new(); + + private static readonly char[] s_invalidRuleIdChars = new char[] { '.', ':', '-', '#' }; + + private static readonly char[] s_slashBackSlash = new char[] { '\\', '/' }; + + #endregion + } +} diff --git a/src/libraries/System.Speech/src/Internal/StreamMarshaler.cs b/src/libraries/System.Speech/src/Internal/StreamMarshaler.cs new file mode 100644 index 00000000000000..4742ff49787c4f --- /dev/null +++ b/src/libraries/System.Speech/src/Internal/StreamMarshaler.cs @@ -0,0 +1,173 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.IO; +using System.Runtime.InteropServices; +using System.Text; + +namespace System.Speech.Internal +{ + internal sealed class StreamMarshaler : IDisposable + { + #region Constructors + + internal StreamMarshaler() + { + } + + internal StreamMarshaler(Stream stream) + { + _stream = stream; + } + + public void Dispose() + { + _safeHMem.Dispose(); + } + + #endregion + + #region internal Methods + internal void ReadArray(T[] ao, int c) + { + Type type = typeof(T); + int sizeOfOne = Marshal.SizeOf(type); + int sizeObject = sizeOfOne * c; + byte[] ab = Helpers.ReadStreamToByteArray(_stream, sizeObject); + + IntPtr buffer = _safeHMem.Buffer(sizeObject); + + Marshal.Copy(ab, 0, buffer, sizeObject); + for (int i = 0; i < c; i++) + { + ao[i] = (T)Marshal.PtrToStructure((IntPtr)((long)buffer + i * sizeOfOne), type); + } + } + + internal void WriteArray(T[] ao, int c) + { + Type type = typeof(T); + int sizeOfOne = Marshal.SizeOf(type); + int sizeObject = sizeOfOne * c; + byte[] ab = new byte[sizeObject]; + IntPtr buffer = _safeHMem.Buffer(sizeObject); + + for (int i = 0; i < c; i++) + { + Marshal.StructureToPtr(ao[i], (IntPtr)((long)buffer + i * sizeOfOne), false); + } + + Marshal.Copy(buffer, ab, 0, sizeObject); + _stream.Write(ab, 0, sizeObject); + } + + internal void ReadArrayChar(char[] ach, int c) + { + int sizeObject = c * Helpers._sizeOfChar; + + if (sizeObject > 0) + { + byte[] ab = Helpers.ReadStreamToByteArray(_stream, sizeObject); + + IntPtr buffer = _safeHMem.Buffer(sizeObject); + + Marshal.Copy(ab, 0, buffer, sizeObject); + Marshal.Copy(buffer, ach, 0, c); + } + } + +#pragma warning disable 56518 // BinaryReader can't be disposed because underlying stream still in use. + + // Helper method to read a Unicode string from a stream. + internal string ReadNullTerminatedString() + { + BinaryReader br = new(_stream, Encoding.Unicode); + StringBuilder stringBuilder = new(); + + while (true) + { + char c = br.ReadChar(); + if (c == '\0') + { + break; + } + else + { + stringBuilder.Append(c); + } + } + return stringBuilder.ToString(); + } + +#pragma warning restore 56518 + + internal void WriteArrayChar(char[] ach, int c) + { + int sizeObject = c * Helpers._sizeOfChar; + + if (sizeObject > 0) + { + byte[] ab = new byte[sizeObject]; + IntPtr buffer = _safeHMem.Buffer(sizeObject); + + Marshal.Copy(ach, 0, buffer, c); + Marshal.Copy(buffer, ab, 0, sizeObject); + _stream.Write(ab, 0, sizeObject); + } + } + + internal void ReadStream(object o) + { + int sizeObject = Marshal.SizeOf(o.GetType()); + byte[] ab = Helpers.ReadStreamToByteArray(_stream, sizeObject); + + IntPtr buffer = _safeHMem.Buffer(sizeObject); + + Marshal.Copy(ab, 0, buffer, sizeObject); + Marshal.PtrToStructure(buffer, o); + } + + internal void WriteStream(object o) + { + int sizeObject = Marshal.SizeOf(o.GetType()); + byte[] ab = new byte[sizeObject]; + IntPtr buffer = _safeHMem.Buffer(sizeObject); + + Marshal.StructureToPtr(o, buffer, false); + Marshal.Copy(buffer, ab, 0, sizeObject); + + // Read the Header + _stream.Write(ab, 0, sizeObject); + } + + #endregion + + #region internal Fields + + internal Stream Stream + { + get + { + return _stream; + } + } + + internal uint Position + { + set + { + _stream.Position = value; + } + } + + #endregion + + #region Private Fields + + private HGlobalSafeHandle _safeHMem = new(); + + private Stream _stream; + + #endregion + } +} diff --git a/src/libraries/System.Speech/src/Internal/StringBlob.cs b/src/libraries/System.Speech/src/Internal/StringBlob.cs new file mode 100644 index 00000000000000..d80d439bcee80f --- /dev/null +++ b/src/libraries/System.Speech/src/Internal/StringBlob.cs @@ -0,0 +1,219 @@ +// 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; + +namespace System.Speech.Internal +{ + internal class StringBlob + { + #region Constructors + + internal StringBlob() + { + } + + internal StringBlob(char[] pszStringArray) + { + int cch = pszStringArray.Length; + if (cch > 0) + { + // First string is always empty. + if (pszStringArray[0] != 0) + { + throw new FormatException(SR.Get(SRID.RecognizerInvalidBinaryGrammar)); + } + + // First pass to copy data and count strings. + for (int iPos = 1, iEnd = cch, iStart = 1; iPos < iEnd; iPos++) + { + if (pszStringArray[iPos] == '\0') + { + string sWord = new(pszStringArray, iStart, iPos - iStart); + _refStrings.Add(sWord); + _offsetStrings.Add(_totalStringSizes); + _strings.Add(sWord, ++_cWords); + _totalStringSizes += sWord.Length + 1; + iStart = iPos + 1; + } + } + } + } + + #endregion + + #region internal Methods + + // + // The ID for a null string is always 0, the ID for subsequent strings is the + // index of the string + 1; + // + internal int Add(string psz, out int idWord) + { + int offset = 0; + idWord = 0; + if (!string.IsNullOrEmpty(psz)) + { + // Check if the string is already in the table + if (!_strings.TryGetValue(psz, out idWord)) + { + System.Diagnostics.Debug.Assert(_strings.Count == _refStrings.Count); + + // No add it to the string table + idWord = ++_cWords; + offset = _totalStringSizes; + _refStrings.Add(psz); + _offsetStrings.Add(offset); + _strings.Add(psz, _cWords); + _totalStringSizes += psz.Length + 1; + } + else + { + offset = OffsetFromId(idWord); + } + } + + return offset; + } + + // Returns idWord; use IndexFromId to recover string offset + internal int Find(string psz) + { + // Compatibility the SAPI version + if (string.IsNullOrEmpty(psz) || _cWords == 0) + { + return 0; + } + + // Use the dictionary to find the value + int iWord; + return _strings.TryGetValue(psz, out iWord) ? iWord : -1; + } + + internal string this[int index] + { + get + { + if ((index < 1) || index > _cWords) + { + throw new InvalidOperationException(); + } + + return _refStrings[index - 1]; + } + } + + /// + /// Only DEBUG code should use this + /// + internal string FromOffset(int offset) + { + int iPos = 1; + int iWord = 1; + + System.Diagnostics.Debug.Assert(offset > 0); + + if (offset == 1 && _cWords >= 1) + { + return this[iWord]; + } + + foreach (string s in _refStrings) + { + iWord++; + iPos += s.Length + 1; + if (offset == iPos) + { + return this[iWord]; + } + } + return null; + } + + internal int StringSize() + { + return _cWords > 0 ? _totalStringSizes : 0; + } + + internal int SerializeSize() + { + return ((StringSize() * _sizeOfChar + 3) & ~3) / 2; + } + + internal char[] SerializeData() + { + // force a 0xcccc at the end of the buffer if the length is odd + int iEnd = SerializeSize(); + + char[] aData = new char[iEnd]; + + // aData [0] is set by the framework to zero + int iPos = 1; + + foreach (string s in _refStrings) + { + for (int i = 0; i < s.Length; i++) + { + aData[iPos++] = s[i]; + } + aData[iPos++] = '\0'; + } + + if (StringSize() % 2 == 1) + { + aData[iPos++] = (char)0xCCCC; + } + + System.Diagnostics.Debug.Assert(iEnd == 0 || iPos == SerializeSize()); + + return aData; + } + + internal int OffsetFromId(int index) + { + System.Diagnostics.Debug.Assert(index <= _cWords); + if (index > 0) + { + return _offsetStrings[index - 1]; + } + + return 0; + } + + #endregion + + #region internal Properties + + internal int Count + { + get + { + return _cWords; + } + } + + #endregion + + #region Private Fields + + // List of words, end-to-end + private Dictionary _strings = new(); + + // List of indices in the dictionary of words + private List _refStrings = new(); + + // List of indices in the dictionary of words + private List _offsetStrings = new(); + + // Number of words + private int _cWords; + + // Cached value for the total string sizes - The first digit is always zero. + private int _totalStringSizes = 1; + + // .NET is Unicode so 2 bytes per characters + private const int _sizeOfChar = 2; + + #endregion + } +} diff --git a/src/libraries/System.Speech/src/Internal/Synthesis/AudioBase.cs b/src/libraries/System.Speech/src/Internal/Synthesis/AudioBase.cs new file mode 100644 index 00000000000000..d59f8810d1b21e --- /dev/null +++ b/src/libraries/System.Speech/src/Internal/Synthesis/AudioBase.cs @@ -0,0 +1,454 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.IO; +using System.Runtime.InteropServices; + +namespace System.Speech.Internal.Synthesis +{ + /// + /// Encapsulates Waveform Audio Interface playback functions and provides a simple + /// interface for playing audio. + /// + internal abstract class AudioBase + { + #region Constructors + + /// + /// Create an instance of AudioBase. + /// + internal AudioBase() + { + } + + #endregion + + #region Internal Methods + + #region abstract Members + + /// + /// Play a wave file. + /// + internal abstract void Begin(byte[] wfx); + + /// + /// Play a wave file. + /// + internal abstract void End(); + + /// + /// Play a wave file. + /// + internal virtual void Play(IntPtr pBuff, int cb) + { + byte[] buffer = new byte[cb]; + Marshal.Copy(pBuff, buffer, 0, cb); + Play(buffer); + } + + /// + /// Play a wave file. + /// + internal virtual void Play(byte[] buffer) + { + GCHandle gc = GCHandle.Alloc(buffer); + Play(gc.AddrOfPinnedObject(), buffer.Length); + gc.Free(); + } + + /// + /// Pause the playback of a sound. + /// + internal abstract void Pause(); + + /// + /// Resume the playback of a paused sound. + /// + internal abstract void Resume(); + + /// + /// Throw an event synchronized with the audio stream + /// + internal abstract void InjectEvent(TTSEvent ttsEvent); + + /// + /// File operation are synchronous no wait + /// + internal abstract void WaitUntilDone(); + + /// + /// Wait for all the queued buffers to be played + /// + internal abstract void Abort(); + + #endregion + + #region helpers + + internal void PlayWaveFile(AudioData audio) + { + // allocate some memory for the largest header + try + { + // Fake a header for ALaw and ULaw + if (!string.IsNullOrEmpty(audio._mimeType)) + { + WAVEFORMATEX wfx = new(); + + wfx.nChannels = 1; + wfx.nSamplesPerSec = 8000; + wfx.nAvgBytesPerSec = 8000; + wfx.nBlockAlign = 1; + wfx.wBitsPerSample = 8; + wfx.cbSize = 0; + + switch (audio._mimeType) + { + case "audio/basic": + wfx.wFormatTag = (short)AudioFormat.EncodingFormat.ULaw; + break; + + case "audio/x-alaw-basic": + wfx.wFormatTag = (short)AudioFormat.EncodingFormat.ALaw; + break; + + default: + throw new FormatException(SR.Get(SRID.UnknownMimeFormat)); + } + + Begin(wfx.ToBytes()); + try + { + byte[] data = new byte[(int)audio._stream.Length]; + audio._stream.Read(data, 0, data.Length); + Play(data); + } + finally + { + WaitUntilDone(); + End(); + } + } + else + { + BinaryReader br = new(audio._stream); + + try + { + byte[] wfx = GetWaveFormat(br); + + if (wfx == null) + { + throw new FormatException(SR.Get(SRID.NotValidAudioFile, audio._uri.ToString())); + } + + Begin(wfx); + + try + { + while (true) + { + DATAHDR dataHdr = new(); + + // check for the end of file (+8 for the 2 DWORD) + if (audio._stream.Position + 8 >= audio._stream.Length) + { + break; + } + dataHdr._id = br.ReadUInt32(); + dataHdr._len = br.ReadInt32(); + + // Is this the WAVE data? + if (dataHdr._id == DATA_MARKER) + { + byte[] ab = Helpers.ReadStreamToByteArray(audio._stream, dataHdr._len); + Play(ab); + } + else + { + // Skip this RIFF fragment. + audio._stream.Seek(dataHdr._len, SeekOrigin.Current); + } + } + } + finally + { + WaitUntilDone(); + End(); + } + } + finally + { + ((IDisposable)br).Dispose(); + } + } + } + finally + { + audio.Dispose(); + } + } + + internal static byte[] GetWaveFormat(BinaryReader br) + { + // Read the riff Header + RIFFHDR riff = new(); + + riff._id = br.ReadUInt32(); + riff._len = br.ReadInt32(); + riff._type = br.ReadUInt32(); + + if (riff._id != RIFF_MARKER && riff._type != WAVE_MARKER) + { + return null; ; + } + + BLOCKHDR block = new(); + block._id = br.ReadUInt32(); + block._len = br.ReadInt32(); + + if (block._id != FMT_MARKER) + { + return null; ; + } + + // If the format is of type WAVEFORMAT then fake a cbByte with a length of zero + byte[] wfx; + wfx = br.ReadBytes(block._len); + + // Hardcode the value of the size for the structure element + // as the C# compiler pads the structure to the closest 4 or 8 bytes + if (block._len == 16) + { + byte[] wfxTemp = new byte[18]; + Array.Copy(wfx, wfxTemp, 16); + wfx = wfxTemp; + } + return wfx; + } + + internal static void WriteWaveHeader(Stream stream, WAVEFORMATEX waveEx, long position, int cData) + { + RIFFHDR riff = new(0); + BLOCKHDR block = new(0); + DATAHDR dataHdr = new(0); + + int cRiff = Marshal.SizeOf(riff); + int cBlock = Marshal.SizeOf(block); + int cWaveEx = waveEx.Length;// Marshal.SizeOf (waveEx); // The CLR automatically pad the waveEx structure to dword boundary. Force 16. + int cDataHdr = Marshal.SizeOf(dataHdr); + + int total = cRiff + cBlock + cWaveEx + cDataHdr; + + using (MemoryStream memStream = new()) + { + BinaryWriter bw = new(memStream); + try + { + // Write the RIFF section + riff._len = total + cData - 8/* - cRiff*/; // for the "WAVE" 4 characters + bw.Write(riff._id); + bw.Write(riff._len); + bw.Write(riff._type); + + // Write the wave header section + block._len = cWaveEx; + bw.Write(block._id); + bw.Write(block._len); + + // Write the FormatEx structure + bw.Write(waveEx.ToBytes()); + //bw.Write (waveEx.cbSize); + + // Write the data section + dataHdr._len = cData; + bw.Write(dataHdr._id); + bw.Write(dataHdr._len); + + stream.Seek(position, SeekOrigin.Begin); + stream.Write(memStream.GetBuffer(), 0, (int)memStream.Length); + } + finally + { + ((IDisposable)bw).Dispose(); + } + } + } + + #endregion + + #endregion + + #region Internal Property + + internal abstract TimeSpan Duration { get; } + + internal virtual long Position { get { return 0; } } + + internal virtual bool IsAborted + { + get + { + return _aborted; + } + set + { + _aborted = false; + } + } + + internal virtual byte[] WaveFormat { get { return null; } } + + #endregion + + #region Protected Property + + protected bool _aborted; + + #endregion + + #region Private Types + + private const uint RIFF_MARKER = 0x46464952; + private const uint WAVE_MARKER = 0x45564157; + private const uint FMT_MARKER = 0x20746d66; + private const uint DATA_MARKER = 0x61746164; + + [StructLayout(LayoutKind.Sequential)] + private struct RIFFHDR + { + internal uint _id; + internal int _len; /* file length less header */ + internal uint _type; /* should be "WAVE" */ + + internal RIFFHDR(int length) + { + _id = RIFF_MARKER; + _type = WAVE_MARKER; + _len = length; + } + } + + [StructLayout(LayoutKind.Sequential)] + private struct BLOCKHDR + { + internal uint _id; /* should be "fmt " or "data" */ + internal int _len; /* block size less header */ + + internal BLOCKHDR(int length) + { + _id = FMT_MARKER; + _len = length; + } + }; + + [StructLayout(LayoutKind.Sequential)] + private struct DATAHDR + { + internal uint _id; /* should be "fmt " or "data" */ + internal int _len; /* block size less header */ + + internal DATAHDR(int length) + { + _id = DATA_MARKER; + _len = length; + } + } + + #endregion + } + + #region Internal Methods + + [System.Runtime.InteropServices.TypeLibTypeAttribute(16)] + internal struct WAVEFORMATEX + { + + internal short wFormatTag; + internal short nChannels; + internal int nSamplesPerSec; + internal int nAvgBytesPerSec; + internal short nBlockAlign; + internal short wBitsPerSample; + internal short cbSize; + + internal static WAVEFORMATEX ToWaveHeader(byte[] waveHeader) + { + GCHandle gc = GCHandle.Alloc(waveHeader, GCHandleType.Pinned); + IntPtr ptr = gc.AddrOfPinnedObject(); + WAVEFORMATEX wfx = new(); + wfx.wFormatTag = Marshal.ReadInt16(ptr); + wfx.nChannels = Marshal.ReadInt16(ptr, 2); + wfx.nSamplesPerSec = Marshal.ReadInt32(ptr, 4); + wfx.nAvgBytesPerSec = Marshal.ReadInt32(ptr, 8); + wfx.nBlockAlign = Marshal.ReadInt16(ptr, 12); + wfx.wBitsPerSample = Marshal.ReadInt16(ptr, 14); + wfx.cbSize = Marshal.ReadInt16(ptr, 16); + + if (wfx.cbSize != 0) + { + throw new InvalidOperationException(); + } + gc.Free(); + return wfx; + } + + internal static void AvgBytesPerSec(byte[] waveHeader, out int avgBytesPerSec, out int nBlockAlign) + { + // Hardcode the value of the size for the structure element + // as the C# compiler pads the structure to the closest 4 or 8 bytes + GCHandle gc = GCHandle.Alloc(waveHeader, GCHandleType.Pinned); + IntPtr ptr = gc.AddrOfPinnedObject(); + avgBytesPerSec = Marshal.ReadInt32(ptr, 8); + nBlockAlign = Marshal.ReadInt16(ptr, 12); + gc.Free(); + } + + internal byte[] ToBytes() + { + System.Diagnostics.Debug.Assert(cbSize == 0); + GCHandle gc = GCHandle.Alloc(this, GCHandleType.Pinned); + byte[] ab = ToBytes(gc.AddrOfPinnedObject()); + gc.Free(); + return ab; + } + + internal static byte[] ToBytes(IntPtr waveHeader) + { + // Hardcode the value of the size for the structure element + // as the C# compiler pads the structure to the closest 4 or 8 bytes + + int cbSize = Marshal.ReadInt16(waveHeader, 16); + byte[] ab = new byte[18 + cbSize]; + Marshal.Copy(waveHeader, ab, 0, 18 + cbSize); + return ab; + } + + internal static WAVEFORMATEX Default + { + get + { + WAVEFORMATEX wfx = new(); + wfx.wFormatTag = 1; + wfx.nChannels = 1; + wfx.nSamplesPerSec = 22050; + wfx.nAvgBytesPerSec = 44100; + wfx.nBlockAlign = 2; + wfx.wBitsPerSample = 16; + wfx.cbSize = 0; + return wfx; + } + } + + internal int Length + { + get + { + return 18 + cbSize; + } + } + } + + #endregion +} diff --git a/src/libraries/System.Speech/src/Internal/Synthesis/AudioDeviceOut.cs b/src/libraries/System.Speech/src/Internal/Synthesis/AudioDeviceOut.cs new file mode 100644 index 00000000000000..6d0e59f2786025 --- /dev/null +++ b/src/libraries/System.Speech/src/Internal/Synthesis/AudioDeviceOut.cs @@ -0,0 +1,509 @@ +// 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.Runtime.InteropServices; +using System.Threading; + +namespace System.Speech.Internal.Synthesis +{ + /// + /// Encapsulates Waveform Audio Interface playback functions and provides a simple + /// interface for playing audio. + /// + internal class AudioDeviceOut : AudioBase, IDisposable + { + #region Constructors + + /// + /// Create an instance of AudioDeviceOut. + /// + internal AudioDeviceOut(int curDevice, IAsyncDispatch asyncDispatch) + { + _delegate = new SafeNativeMethods.WaveOutProc(CallBackProc); + _asyncDispatch = asyncDispatch; + _curDevice = curDevice; + } + + ~AudioDeviceOut() + { + Dispose(false); + } + public void Dispose() + { + Dispose(true); + GC.SuppressFinalize(this); + } + + private void Dispose(bool disposing) + { + if (_deviceOpen && _hwo != IntPtr.Zero) + { + SafeNativeMethods.waveOutClose(_hwo); + _deviceOpen = false; + } + if (disposing) + { + ((IDisposable)_evt).Dispose(); + } + } + + #endregion + + #region Internal Methods + + #region AudioDevice implementation + + /// + /// Begin to play + /// + internal override void Begin(byte[] wfx) + { + if (_deviceOpen) + { + System.Diagnostics.Debug.Assert(false); + throw new InvalidOperationException(); + } + + // Get the alignments values + WAVEFORMATEX.AvgBytesPerSec(wfx, out _nAvgBytesPerSec, out _blockAlign); + + MMSYSERR result; + lock (_noWriteOutLock) + { + result = SafeNativeMethods.waveOutOpen(ref _hwo, _curDevice, wfx, _delegate, IntPtr.Zero, SafeNativeMethods.CALLBACK_FUNCTION); + + if (_fPaused && result == MMSYSERR.NOERROR) + { + result = SafeNativeMethods.waveOutPause(_hwo); + } + // set the flags + _aborted = false; + _deviceOpen = true; + } + + if (result != MMSYSERR.NOERROR) + { + throw new AudioException(result); + } + + // Reset the counter for the number of bytes written so far + _bytesWritten = 0; + + // Nothing in the queue + _evt.Set(); + } + + /// + /// Begin to play + /// + internal override void End() + { + if (!_deviceOpen) + { + System.Diagnostics.Debug.Assert(false); + throw new InvalidOperationException(); + } + lock (_noWriteOutLock) + { + _deviceOpen = false; + + MMSYSERR result; + + CheckForAbort(); + + if (_queueIn.Count != 0) + { + SafeNativeMethods.waveOutReset(_hwo); + } + + // Close it; no point in returning errors if this fails + result = SafeNativeMethods.waveOutClose(_hwo); + + if (result != MMSYSERR.NOERROR) + { + // This may create a dead lock + System.Diagnostics.Debug.Assert(false); + } + } + } + + /// + /// Play a wave file. + /// + internal override void Play(byte[] buffer) + { + if (!_deviceOpen) + { + System.Diagnostics.Debug.Assert(false); + } + else + { + int bufferSize = buffer.Length; + _bytesWritten += bufferSize; + + System.Diagnostics.Debug.Assert(bufferSize % _blockAlign == 0); + + WaveHeader waveHeader = new(buffer); + GCHandle waveHdr = waveHeader.WAVEHDR; + MMSYSERR result = SafeNativeMethods.waveOutPrepareHeader(_hwo, waveHdr.AddrOfPinnedObject(), waveHeader.SizeHDR); + + if (result != MMSYSERR.NOERROR) + { + throw new AudioException(result); + } + + lock (_noWriteOutLock) + { + if (!_aborted) + { + lock (_queueIn) + { + InItem item = new(waveHeader); + + _queueIn.Add(item); + + // Something in the queue cannot exit anymore + _evt.Reset(); + } + + // Start playback of the first buffer + result = SafeNativeMethods.waveOutWrite(_hwo, waveHdr.AddrOfPinnedObject(), waveHeader.SizeHDR); + if (result != MMSYSERR.NOERROR) + { + lock (_queueIn) + { + _queueIn.RemoveAt(_queueIn.Count - 1); + throw new AudioException(result); + } + } + } + } + } + } + + /// + /// Pause the playback of a sound. + /// + internal override void Pause() + { + lock (_noWriteOutLock) + { + if (!_aborted && !_fPaused) + { + if (_deviceOpen) + { + MMSYSERR result = SafeNativeMethods.waveOutPause(_hwo); + if (result != MMSYSERR.NOERROR) + { + System.Diagnostics.Debug.Assert(false, ((int)result).ToString(System.Globalization.CultureInfo.InvariantCulture)); + } + } + _fPaused = true; + } + } + } + + /// + /// Resume the playback of a paused sound. + /// + internal override void Resume() + { + lock (_noWriteOutLock) + { + if (!_aborted && _fPaused) + { + if (_deviceOpen) + { + MMSYSERR result = SafeNativeMethods.waveOutRestart(_hwo); + if (result != MMSYSERR.NOERROR) + { + System.Diagnostics.Debug.Assert(false); + } + } + } + } + _fPaused = false; + } + + /// + /// Wait for all the queued buffers to be played + /// + internal override void Abort() + { + lock (_noWriteOutLock) + { + _aborted = true; + if (_queueIn.Count > 0) + { + SafeNativeMethods.waveOutReset(_hwo); + _evt.WaitOne(); + } + } + } + + internal override void InjectEvent(TTSEvent ttsEvent) + { + if (_asyncDispatch != null && !_aborted) + { + lock (_queueIn) + { + // Throw immediately if the queue is empty + if (_queueIn.Count == 0) + { + _asyncDispatch.Post(ttsEvent); + } + else + { + // Will be thrown before the next write to the audio device + _queueIn.Add(new InItem(ttsEvent)); + } + } + } + } + + /// + /// Wait for all the queued buffers to be played + /// + internal override void WaitUntilDone() + { + if (!_deviceOpen) + { + System.Diagnostics.Debug.Assert(false); + throw new InvalidOperationException(); + } + + _evt.WaitOne(); + } + + #endregion + + #region Audio device specific methods + + /// + /// Determine the number of available playback devices. + /// + /// Number of output devices + internal static int NumDevices() + { + return SafeNativeMethods.waveOutGetNumDevs(); + } + + internal static int GetDevicedId(string name) + { + for (int iDevice = 0; iDevice < NumDevices(); iDevice++) + { + string device; + if (GetDeviceName(iDevice, out device) == MMSYSERR.NOERROR && string.Compare(device, name, StringComparison.OrdinalIgnoreCase) == 0) + { + return iDevice; + } + } + return -1; + } + + /// + /// Get the name of the specified playback device. + /// + /// ID of the device + /// Destination string assigned the name + /// MMSYSERR.NOERROR if successful + internal static MMSYSERR GetDeviceName(int deviceId, [MarshalAs(UnmanagedType.LPWStr)] out string prodName) + { + prodName = string.Empty; + SafeNativeMethods.WAVEOUTCAPS caps = new(); + + MMSYSERR result = SafeNativeMethods.waveOutGetDevCaps((IntPtr)deviceId, ref caps, Marshal.SizeOf(caps)); + if (result != MMSYSERR.NOERROR) + { + return result; + } + + prodName = caps.szPname; + + return MMSYSERR.NOERROR; + } + + #endregion + + #endregion + + #region Internal Fields + + internal override TimeSpan Duration + { + get + { + if (_nAvgBytesPerSec == 0) + { + return new TimeSpan(0); + } + return new TimeSpan((_bytesWritten * TimeSpan.TicksPerSecond) / _nAvgBytesPerSec); + } + } + + #endregion + + #region Private Methods + + private void CallBackProc(IntPtr hwo, MM_MSG uMsg, IntPtr dwInstance, IntPtr dwParam1, IntPtr dwParam2) + { + if (uMsg == MM_MSG.MM_WOM_DONE) + { + InItem inItem; + lock (_queueIn) + { + inItem = _queueIn[0]; + inItem.ReleaseData(); + _queueIn.RemoveAt(0); + _queueOut.Add(inItem); + + // look for the next elements in the queue if they are events to throw! + while (_queueIn.Count > 0) + { + inItem = _queueIn[0]; + // Do we have an event or a sound buffer + if (inItem._waveHeader == null) + { + if (_asyncDispatch != null && !_aborted) + { + _asyncDispatch.Post(inItem._userData); + } + _queueIn.RemoveAt(0); + } + else + { + break; + } + } + } + + // if the queue is empty, then restart the callers thread + if (_queueIn.Count == 0) + { + _evt.Set(); + } + } + } + + private void ClearBuffers() + { + foreach (InItem item in _queueOut) + { + WaveHeader waveHeader = item._waveHeader; + MMSYSERR result; + + result = SafeNativeMethods.waveOutUnprepareHeader( + _hwo, waveHeader.WAVEHDR.AddrOfPinnedObject(), waveHeader.SizeHDR); + if (result != MMSYSERR.NOERROR) + { + //System.Diagnostics.Debug.Assert (false); + } + waveHeader.Dispose(); + } + + _queueOut.Clear(); + } + + private void CheckForAbort() + { + if (_aborted) + { + // Synchronous operation + lock (_queueIn) + { + foreach (InItem inItem in _queueIn) + { + // Do we have an event or a sound buffer + if (inItem._waveHeader != null) + { + WaveHeader waveHeader = inItem._waveHeader; + SafeNativeMethods.waveOutUnprepareHeader( + _hwo, waveHeader.WAVEHDR.AddrOfPinnedObject(), waveHeader.SizeHDR); + waveHeader.Dispose(); + } + else + { + _asyncDispatch.Post(inItem._userData); + } + } + _queueIn.Clear(); + + // if the queue is empty, then restart the callers thread + _evt.Set(); + } + } + ClearBuffers(); + } + + #endregion + + #region Private Types + + /// + /// This object must keep a reference to the waveHeader object + /// so that the pinned buffer containing the data is not + /// released before it is finished being played + /// + private class InItem : IDisposable + { + internal InItem(WaveHeader waveHeader) + { + _waveHeader = waveHeader; + } + + internal InItem(object userData) + { + _userData = userData; + } + public void Dispose() + { + if (_waveHeader != null) + { + _waveHeader.Dispose(); + } + + GC.SuppressFinalize(this); + } + + internal void ReleaseData() + { + if (_waveHeader != null) + { + _waveHeader.ReleaseData(); + } + } + + internal WaveHeader _waveHeader; + internal object _userData; + } + + #endregion + + #region Private Fields + + private List _queueIn = new(); + + private List _queueOut = new(); + + private int _blockAlign; + private int _bytesWritten; + private int _nAvgBytesPerSec; + + private IntPtr _hwo; + + private int _curDevice; + + private ManualResetEvent _evt = new(false); + + private SafeNativeMethods.WaveOutProc _delegate; + + private IAsyncDispatch _asyncDispatch; + + private bool _deviceOpen; + private object _noWriteOutLock = new(); + private bool _fPaused; + + #endregion + } +} diff --git a/src/libraries/System.Speech/src/Internal/Synthesis/AudioException.cs b/src/libraries/System.Speech/src/Internal/Synthesis/AudioException.cs new file mode 100644 index 00000000000000..c766cd484dc9b8 --- /dev/null +++ b/src/libraries/System.Speech/src/Internal/Synthesis/AudioException.cs @@ -0,0 +1,24 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Runtime.Serialization; + +namespace System.Speech.Internal.Synthesis +{ + [Serializable] + internal class AudioException : Exception + { + #region Constructors + internal AudioException() + { + } + internal AudioException(MMSYSERR errorCode) : base(string.Format(System.Globalization.CultureInfo.InvariantCulture, "{0} - Error Code: 0x{1:x}", SR.Get(SRID.AudioDeviceError), (int)errorCode)) + { + } + protected AudioException(SerializationInfo info, StreamingContext context) : base(info, context) + { + } + + #endregion + } +} diff --git a/src/libraries/System.Speech/src/Internal/Synthesis/AudioFileOut.cs b/src/libraries/System.Speech/src/Internal/Synthesis/AudioFileOut.cs new file mode 100644 index 00000000000000..ca1382eeecf3f3 --- /dev/null +++ b/src/libraries/System.Speech/src/Internal/Synthesis/AudioFileOut.cs @@ -0,0 +1,261 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.IO; +using System.Speech.AudioFormat; +using System.Threading; + +namespace System.Speech.Internal.Synthesis +{ + /// + /// Encapsulates Waveform Audio Interface playback functions and provides a simple + /// interface for playing audio. + /// + internal class AudioFileOut : AudioBase, IDisposable + { + #region Constructors + + /// + /// Create an instance of AudioFileOut. + /// + internal AudioFileOut(Stream stream, SpeechAudioFormatInfo formatInfo, bool headerInfo, IAsyncDispatch asyncDispatch) + { + _asyncDispatch = asyncDispatch; + _stream = stream; + _startStreamPosition = _stream.Position; + _hasHeader = headerInfo; + + _wfxOut = new WAVEFORMATEX(); + // if we have a formatInfo object, format conversion may be necessary + if (formatInfo != null) + { + // Build the Wave format from the formatInfo + _wfxOut.wFormatTag = (short)formatInfo.EncodingFormat; + _wfxOut.wBitsPerSample = (short)formatInfo.BitsPerSample; + _wfxOut.nSamplesPerSec = formatInfo.SamplesPerSecond; + _wfxOut.nChannels = (short)formatInfo.ChannelCount; + } + else + { + // Set the default values + _wfxOut = WAVEFORMATEX.Default; + } + _wfxOut.nBlockAlign = (short)(_wfxOut.nChannels * _wfxOut.wBitsPerSample / 8); + _wfxOut.nAvgBytesPerSec = _wfxOut.wBitsPerSample * _wfxOut.nSamplesPerSec * _wfxOut.nChannels / 8; + } + + public void Dispose() + { + _evt.Close(); + GC.SuppressFinalize(this); + } + + #endregion + + #region Internal Methods + + /// + /// Begin to play + /// + internal override void Begin(byte[] wfx) + { + if (_deviceOpen) + { + System.Diagnostics.Debug.Assert(false); + throw new InvalidOperationException(); + } + + // Get the audio format if conversion is needed + _wfxIn = WAVEFORMATEX.ToWaveHeader(wfx); + _doConversion = _pcmConverter.PrepareConverter(ref _wfxIn, ref _wfxOut); + + if (_totalByteWrittens == 0 && _hasHeader) + { + WriteWaveHeader(_stream, _wfxOut, _startStreamPosition, 0); + } + + _bytesWritten = 0; + + // set the flags + _aborted = false; + _deviceOpen = true; + } + + /// + /// Begin to play + /// + internal override void End() + { + if (!_deviceOpen) + { + System.Diagnostics.Debug.Assert(false); + throw new InvalidOperationException(); + } + _deviceOpen = false; + + if (!_aborted) + { + if (_hasHeader) + { + long position = _stream.Position; + WriteWaveHeader(_stream, _wfxOut, _startStreamPosition, _totalByteWrittens); + _stream.Seek(position, SeekOrigin.Begin); + } + } + } + + #region AudioDevice implementation + + /// + /// Play a wave file. + /// + internal override void Play(byte[] buffer) + { + if (!_deviceOpen) + { + System.Diagnostics.Debug.Assert(false); + } + else + { + byte[] abOut = _doConversion ? _pcmConverter.ConvertSamples(buffer) : buffer; + + if (_paused) + { + _evt.WaitOne(); + _evt.Reset(); + } + if (!_aborted) + { + _stream.Write(abOut, 0, abOut.Length); + _totalByteWrittens += abOut.Length; + _bytesWritten += abOut.Length; + } + } + } + + /// + /// Pause the playback of a sound. + /// + internal override void Pause() + { + if (!_aborted && !_paused) + { + lock (_noWriteOutLock) + { + _paused = true; + } + } + } + + /// + /// Resume the playback of a paused sound. + /// + internal override void Resume() + { + if (!_aborted && _paused) + { + lock (_noWriteOutLock) + { + _paused = false; + _evt.Set(); + } + } + } + + /// + /// Wait for all the queued buffers to be played + /// + internal override void Abort() + { + lock (_noWriteOutLock) + { + _aborted = true; + _paused = false; + _evt.Set(); + } + } + + internal override void InjectEvent(TTSEvent ttsEvent) + { + if (!_aborted && _asyncDispatch != null) + { + _asyncDispatch.Post(ttsEvent); + } + } + + /// + /// File operation are basically synchronous + /// + internal override void WaitUntilDone() + { + lock (_noWriteOutLock) + { + } + } + + #endregion + + #endregion + + #region Internal Fields + + internal override TimeSpan Duration + { + get + { + if (_wfxIn.nAvgBytesPerSec == 0) + { + return new TimeSpan(0); + } + return new TimeSpan((_bytesWritten * TimeSpan.TicksPerSecond) / _wfxIn.nAvgBytesPerSec); + } + } + + internal override long Position + { + get + { + return _stream.Position; + } + } + + internal override byte[] WaveFormat + { + get + { + return _wfxOut.ToBytes(); + } + } + + #endregion + + #region Private Fields + + protected ManualResetEvent _evt = new(false); + protected bool _deviceOpen; + + protected Stream _stream; + + protected PcmConverter _pcmConverter = new(); + protected bool _doConversion; + + protected bool _paused; + protected int _totalByteWrittens; + protected int _bytesWritten; + + #endregion + + #region Private Fields + + private IAsyncDispatch _asyncDispatch; + private object _noWriteOutLock = new(); + + private WAVEFORMATEX _wfxIn; + private WAVEFORMATEX _wfxOut; + private bool _hasHeader; + + private long _startStreamPosition; + + #endregion + } +} diff --git a/src/libraries/System.Speech/src/Internal/Synthesis/AudioFormatConverter.cs b/src/libraries/System.Speech/src/Internal/Synthesis/AudioFormatConverter.cs new file mode 100644 index 00000000000000..819f7c98050359 --- /dev/null +++ b/src/libraries/System.Speech/src/Internal/Synthesis/AudioFormatConverter.cs @@ -0,0 +1,612 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +#region Using directives + +#endregion + +namespace System.Speech.Internal.Synthesis +{ + /// + /// AudioFormatConverter takes its conversion tables from ...\scg\tts\common\vapiio\alaw_ULaw.cpp + /// + internal static class AudioFormatConverter + { + #region Internal Methods + + /// + /// Finds the converting method based on the specified formats. + /// + /// Reference to the buffer of audio data. + /// Audio format that the data will be converted from. + /// Audio format that the data will be converted to. + /// New array with the audio data in requested format. + internal static short[] Convert(byte[] data, AudioCodec from, AudioCodec to) + { + ConvertByteShort cnvDlgt = null; + + switch (from) + { + case AudioCodec.PCM8: + switch (to) + { + case AudioCodec.PCM16: cnvDlgt = new ConvertByteShort(ConvertLinear8LinearByteShort); break; + } + break; + case AudioCodec.PCM16: + switch (to) + { + case AudioCodec.PCM16: cnvDlgt = new ConvertByteShort(ConvertLinear2LinearByteShort); break; + } + break; + + case AudioCodec.G711U: + switch (to) + { + case AudioCodec.PCM16: cnvDlgt = new ConvertByteShort(ConvertULaw2Linear); break; + } + break; + case AudioCodec.G711A: + switch (to) + { + case AudioCodec.PCM16: cnvDlgt = new ConvertByteShort(ConvertALaw2Linear); break; + } + break; + + default: + throw new FormatException(); + } + + if (cnvDlgt == null) + { + throw new FormatException(); + } + + return cnvDlgt(data, data.Length); + } + + /// + /// Finds the converting method based on the specified formats. + /// + /// Reference to the buffer of audio data. + /// Audio format that the data will be converted from. + /// Audio format that the data will be converted to. + /// New array with the audio data in requested format. + internal static byte[] Convert(short[] data, AudioCodec from, AudioCodec to) + { + ConvertShortByte cnvDlgt = null; + + switch (from) + { + case AudioCodec.PCM16: + switch (to) + { + case AudioCodec.PCM8: cnvDlgt = new ConvertShortByte(ConvertLinear8LinearShortByte); break; + case AudioCodec.PCM16: cnvDlgt = new ConvertShortByte(ConvertLinear2LinearShortByte); break; + case AudioCodec.G711U: cnvDlgt = new ConvertShortByte(ConvertLinear2ULaw); break; + case AudioCodec.G711A: cnvDlgt = new ConvertShortByte(ConvertLinear2ALaw); break; + } + break; + + default: + throw new FormatException(); + } + + return cnvDlgt(data, data.Length); + } + + internal static AudioCodec TypeOf(WAVEFORMATEX format) + { + AudioCodec codec = AudioCodec.Undefined; + + switch ((WaveFormatTag)format.wFormatTag) + { + case WaveFormatTag.WAVE_FORMAT_PCM: + switch (format.nBlockAlign / format.nChannels) + { + case 1: + codec = AudioCodec.PCM8; + break; + case 2: + codec = AudioCodec.PCM16; + break; + } + break; + + case WaveFormatTag.WAVE_FORMAT_ALAW: + codec = AudioCodec.G711A; + break; + + case WaveFormatTag.WAVE_FORMAT_MULAW: + codec = AudioCodec.G711U; + break; + } + + return codec; + } + + #endregion + + #region Private Methods + + #region Converters between Linear and ULaw + + /// + /// This routine converts from 16 bit linear to ULaw by direct access to the conversion table. + /// + /// Array of 16 bit linear samples. + /// Size of the data in the array. + /// New buffer of 8 bit ULaw samples. + internal static byte[] ConvertLinear2ULaw(short[] data, int size) + { + byte[] newData = new byte[size]; + s_uLawCompTableCached = s_uLawCompTableCached == null ? CalcLinear2ULawTable() : s_uLawCompTableCached; + + for (int i = 0; i < size; i++) + { + unchecked + { + // Extend the sign bit for the sample that is constructed from two bytes + newData[i] = s_uLawCompTableCached[(ushort)data[i] >> 2]; + } + } + return newData; + } + + /// + /// This routine converts from ULaw to 16 bit linear by direct access to the conversion table. + /// + /// Array of 8 bit ULaw samples. + /// Size of the data in the array. + /// New buffer of signed 16 bit linear samples + internal static short[] ConvertULaw2Linear(byte[] data, int size) + { + short[] newData = new short[size]; + for (int i = 0; i < size; i++) + { + int sample = s_ULaw_exp_table[data[i]]; + + newData[i] = unchecked((short)sample); + } + + return newData; + } + + /// + /// This routine converts from linear to ULaw. + /// + /// Craig Reese: IDA/Supercomputing Research Center + /// Joe Campbell: Department of Defense + /// 29 September 1989 + /// + /// References: + /// 1) CCITT Recommendation G.711 (very difficult to follow) + /// 2) "A New Digital Technique for Implementation of Any + /// Continuous PCM Companding Law," Villeret, Michel, + /// et al. 1973 IEEE Int. Conf. on Communications, Vol 1, + /// 1973, pg. 11.12-11.17 + /// 3) MIL-STD-188-113,"Interoperability and Performance Standards + /// for Analog-to_Digital Conversion Techniques," + /// 17 February 1987 + /// + /// New buffer of 8 bit ULaw samples + private static byte[] CalcLinear2ULawTable() + { + /*const*/ + bool ZEROTRAP = false; // turn off the trap as per the MIL-STD + const byte uBIAS = 0x84; // define the add-in bias for 16 bit samples + const int uCLIP = 32635; + + byte[] table = new byte[(ushort.MaxValue + 1) >> 2]; + + for (int i = 0; i < ushort.MaxValue; i += 4) + { + short data = unchecked((short)i); + + int sample; + int sign, exponent, mantissa; + byte ULawbyte; + + unchecked + { + // Extend the sign bit for the sample that is constructed from two bytes + sample = (data >> 2) << 2; + + // Get the sample into sign-magnitude. + sign = (sample >> 8) & 0x80; // set aside the sign + if (sign != 0) + { + sample = -sample; + } + if (sample > uCLIP) sample = uCLIP; // clip the magnitude + + // Convert from 16 bit linear to ULaw. + sample = sample + uBIAS; + exponent = s_exp_lut_linear2ulaw[(sample >> 7) & 0xFF]; + mantissa = (sample >> (exponent + 3)) & 0x0F; + + ULawbyte = (byte)(~(sign | (exponent << 4) | mantissa)); + } + + if (ZEROTRAP) + { + if (ULawbyte == 0) ULawbyte = 0x02; // optional CCITT trap + } + + table[i >> 2] = ULawbyte; + } + + return table; + } + + #endregion + + #region Converters between Linear and ALaw + + /// + /// This routine converts from 16 bit linear to ALaw by direct access to the conversion table. + /// + /// Array of 16 bit linear samples. + /// Size of the data in the array. + /// New buffer of 8 bit ALaw samples. + internal static byte[] ConvertLinear2ALaw(short[] data, int size) + { + byte[] newData = new byte[size]; + s_aLawCompTableCached = s_aLawCompTableCached == null ? CalcLinear2ALawTable() : s_aLawCompTableCached; + + for (int i = 0; i < size; i++) + { + unchecked + { + //newData [i] = ALaw_comp_table [(data [i] / 4) & 0x3fff]; + newData[i] = s_aLawCompTableCached[(ushort)data[i] >> 2]; + } + } + return newData; + } + + /// + /// This routine converts from ALaw to 16 bit linear by direct access to the conversion table. + /// + /// Array of 8 bit ALaw samples. + /// Size of the data in the array. + /// New buffer of signed 16 bit linear samples + internal static short[] ConvertALaw2Linear(byte[] data, int size) + { + short[] newData = new short[size]; + for (int i = 0; i < size; i++) + { + int sample = s_ALaw_exp_table[data[i]]; + + newData[i] = unchecked((short)sample); + } + + return newData; + } + + /// + /// This routine converts from linear to ALaw. + /// + /// Craig Reese: IDA/Supercomputing Research Center + /// Joe Campbell: Department of Defense + /// 29 September 1989 + /// + /// References: + /// 1) CCITT Recommendation G.711 (very difficult to follow) + /// 2) "A New Digital Technique for Implementation of Any + /// Continuous PCM Companding Law," Villeret, Michel, + /// et al. 1973 IEEE Int. Conf. on Communications, Vol 1, + /// 1973, pg. 11.12-11.17 + /// 3) MIL-STD-188-113,"Interoperability and Performance Standards + /// for Analog-to_Digital Conversion Techniques," + /// 17 February 1987 + /// + /// New buffer of 8 bit ALaw samples + private static byte[] CalcLinear2ALawTable() + { + const int ACLIP = 31744; + + byte[] table = new byte[(ushort.MaxValue + 1) >> 2]; + + for (int i = 0; i < ushort.MaxValue; i += 4) + { + short data = unchecked((short)i); + + int sample, sign, exponent, mantissa; + byte ALawbyte; + + unchecked + { + // Extend the sign bit for the sample that is constructed from two bytes + sample = (data >> 2) << 2; + + // Get the sample into sign-magnitude. + sign = ((~sample) >> 8) & 0x80; // set aside the sign + if (sign == 0) sample = -sample; // get magnitude + if (sample > ACLIP) sample = ACLIP; // clip the magnitude + } + + // Convert from 16 bit linear to ULaw. + if (sample >= 256) + { + exponent = s_exp_lut_linear2alaw[(sample >> 8) & 0x7F]; + mantissa = (sample >> (exponent + 3)) & 0x0F; + ALawbyte = (byte)((exponent << 4) | mantissa); + } + else + { + ALawbyte = (byte)(sample >> 4); + } + + ALawbyte ^= (byte)(sign ^ 0x55); + + table[i >> 2] = ALawbyte; + } + + return table; + } + + #endregion + + #region PCM to PCM + + /// + /// Empty linear conversion (does nothing, for table consistency). + /// + /// Array of audio data in linear format. + /// Size of the data in the array. + /// The same array in linear format. + private static short[] ConvertLinear2LinearByteShort(byte[] data, int size) + { + short[] as1 = new short[size / 2]; + unchecked + { + for (int i = 0; i < size; i += 2) + { + as1[i / 2] = (short)(data[i] + (short)(data[i + 1] << 8)); + } + } + return as1; + } + + /// + /// Empty linear conversion (does nothing, for table consistency). + /// + /// Array of audio data in linear format. + /// Size of the data in the array. + /// The same array in linear format. + private static short[] ConvertLinear8LinearByteShort(byte[] data, int size) + { + short[] as1 = new short[size]; + unchecked + { + for (int i = 0; i < size; i++) + { + as1[i] = (short)((data[i] - 128) << 8); + } + } + return as1; + } + + /// + /// Empty linear conversion (does nothing, for table consistency). + /// + /// Array of audio data in linear format. + /// Size of the data in the array. + /// The same array in linear format. + private static byte[] ConvertLinear2LinearShortByte(short[] data, int size) + { + byte[] ab = new byte[size * 2]; + for (int i = 0; i < size; i++) + { + short s = data[i]; + ab[2 * i] = unchecked((byte)s); + ab[2 * i + 1] = unchecked((byte)(s >> 8)); + } + return ab; // the same format: do nothing + } + + /// + /// Empty linear conversion (does nothing, for table consistency). + /// + /// Array of audio data in linear format. + /// Size of the data in the array. + /// The same array in linear format. + private static byte[] ConvertLinear8LinearShortByte(short[] data, int size) + { + byte[] ab = new byte[size]; + for (int i = 0; i < size; i++) + { + ab[i] = unchecked((byte)(((ushort)((data[i] + 127) >> 8)) + 128)); + } + return ab; // the same format: do nothing + } + + #endregion + + #endregion + + #region Private Members + + #region Conversion tables for direct conversions + + // Cached table for aLaw and uLaw conversion (16K * 2 bytes each) + private static byte[] s_uLawCompTableCached; + private static byte[] s_aLawCompTableCached; + + #endregion + + #region Conversion tables for algorithmic conversions + + private static readonly int[] s_exp_lut_linear2alaw = new int[128] + { + 1, 1, 2, 2, 3, 3, 3, 3, + 4, 4, 4, 4, 4, 4, 4, 4, + 5, 5, 5, 5, 5, 5, 5, 5, + 5, 5, 5, 5, 5, 5, 5, 5, + 6, 6, 6, 6, 6, 6, 6, 6, + 6, 6, 6, 6, 6, 6, 6, 6, + 6, 6, 6, 6, 6, 6, 6, 6, + 6, 6, 6, 6, 6, 6, 6, 6, + 7, 7, 7, 7, 7, 7, 7, 7, + 7, 7, 7, 7, 7, 7, 7, 7, + 7, 7, 7, 7, 7, 7, 7, 7, + 7, 7, 7, 7, 7, 7, 7, 7, + 7, 7, 7, 7, 7, 7, 7, 7, + 7, 7, 7, 7, 7, 7, 7, 7, + 7, 7, 7, 7, 7, 7, 7, 7, + 7, 7, 7, 7, 7, 7, 7, 7 + }; + + private static int[] s_exp_lut_linear2ulaw = new int[256] + { + 0, 0, 1, 1, 2, 2, 2, 2, 3, 3, 3, 3, 3, 3, 3, 3, + 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, + 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, + 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, + 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, + 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, + 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, + 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, + 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, + 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, + 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, + 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, + 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, + 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, + 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, + 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7 + }; + + #endregion + + #region Conversion tables for 'byte' to 'short' conversion + + /// + /// Table to converts ULaw values to Linear + /// + private static int[] s_ULaw_exp_table = new int[256] + { + -32124, -31100, -30076, -29052, -28028, -27004, -25980, -24956, + -23932, -22908, -21884, -20860, -19836, -18812, -17788, -16764, + -15996, -15484, -14972, -14460, -13948, -13436, -12924, -12412, + -11900, -11388, -10876, -10364, -9852, -9340, -8828, -8316, + -7932, -7676, -7420, -7164, -6908, -6652, -6396, -6140, + -5884, -5628, -5372, -5116, -4860, -4604, -4348, -4092, + -3900, -3772, -3644, -3516, -3388, -3260, -3132, -3004, + -2876, -2748, -2620, -2492, -2364, -2236, -2108, -1980, + -1884, -1820, -1756, -1692, -1628, -1564, -1500, -1436, + -1372, -1308, -1244, -1180, -1116, -1052, -988, -924, + -876, -844, -812, -780, -748, -716, -684, -652, + -620, -588, -556, -524, -492, -460, -428, -396, + -372, -356, -340, -324, -308, -292, -276, -260, + -244, -228, -212, -196, -180, -164, -148, -132, + -120, -112, -104, -96, -88, -80, -72, -64, + -56, -48, -40, -32, -24, -16, -8, 0, + 32124, 31100, 30076, 29052, 28028, 27004, 25980, 24956, + 23932, 22908, 21884, 20860, 19836, 18812, 17788, 16764, + 15996, 15484, 14972, 14460, 13948, 13436, 12924, 12412, + 11900, 11388, 10876, 10364, 9852, 9340, 8828, 8316, + 7932, 7676, 7420, 7164, 6908, 6652, 6396, 6140, + 5884, 5628, 5372, 5116, 4860, 4604, 4348, 4092, + 3900, 3772, 3644, 3516, 3388, 3260, 3132, 3004, + 2876, 2748, 2620, 2492, 2364, 2236, 2108, 1980, + 1884, 1820, 1756, 1692, 1628, 1564, 1500, 1436, + 1372, 1308, 1244, 1180, 1116, 1052, 988, 924, + 876, 844, 812, 780, 748, 716, 684, 652, + 620, 588, 556, 524, 492, 460, 428, 396, + 372, 356, 340, 324, 308, 292, 276, 260, + 244, 228, 212, 196, 180, 164, 148, 132, + 120, 112, 104, 96, 88, 80, 72, 64, + 56, 48, 40, 32, 24, 16, 8, 0 + }; + + /// + /// Table to converts ALaw values to Linear + /// + private static int[] s_ALaw_exp_table = new int[256] + { + -5504, -5248, -6016, -5760, -4480, -4224, -4992, -4736, + -7552, -7296, -8064, -7808, -6528, -6272, -7040, -6784, + -2752, -2624, -3008, -2880, -2240, -2112, -2496, -2368, + -3776, -3648, -4032, -3904, -3264, -3136, -3520, -3392, + -22016, -20992, -24064, -23040, -17920, -16896, -19968, -18944, + -30208, -29184, -32256, -31232, -26112, -25088, -28160, -27136, + -11008, -10496, -12032, -11520, -8960, -8448, -9984, -9472, + -15104, -14592, -16128, -15616, -13056, -12544, -14080, -13568, + -344, -328, -376, -360, -280, -264, -312, -296, + -472, -456, -504, -488, -408, -392, -440, -424, + -88, -72, -120, -104, -24, -8, -56, -40, + -216, -200, -248, -232, -152, -136, -184, -168, + -1376, -1312, -1504, -1440, -1120, -1056, -1248, -1184, + -1888, -1824, -2016, -1952, -1632, -1568, -1760, -1696, + -688, -656, -752, -720, -560, -528, -624, -592, + -944, -912, -1008, -976, -816, -784, -880, -848, + 5504, 5248, 6016, 5760, 4480, 4224, 4992, 4736, + 7552, 7296, 8064, 7808, 6528, 6272, 7040, 6784, + 2752, 2624, 3008, 2880, 2240, 2112, 2496, 2368, + 3776, 3648, 4032, 3904, 3264, 3136, 3520, 3392, + 22016, 20992, 24064, 23040, 17920, 16896, 19968, 18944, + 30208, 29184, 32256, 31232, 26112, 25088, 28160, 27136, + 11008, 10496, 12032, 11520, 8960, 8448, 9984, 9472, + 15104, 14592, 16128, 15616, 13056, 12544, 14080, 13568, + 344, 328, 376, 360, 280, 264, 312, 296, + 472, 456, 504, 488, 408, 392, 440, 424, + 88, 72, 120, 104, 24, 8, 56, 40, + 216, 200, 248, 232, 152, 136, 184, 168, + 1376, 1312, 1504, 1440, 1120, 1056, 1248, 1184, + 1888, 1824, 2016, 1952, 1632, 1568, 1760, 1696, + 688, 656, 752, 720, 560, 528, 624, 592, + 944, 912, 1008, 976, 816, 784, 880, 848 + }; + + #endregion + + internal enum WaveFormatTag + { + WAVE_FORMAT_PCM = 1, + WAVE_FORMAT_ALAW = 0x0006, + WAVE_FORMAT_MULAW = 0x0007 + } + // delegates + private delegate short[] ConvertByteShort(byte[] data, int size); + private delegate byte[] ConvertShortByte(short[] data, int size); + + #endregion + } + + #region Internal Types + + /// + /// Supported formats for audio transcoding in SES + /// + internal enum AudioCodec + { + /// + /// Audio format PCM 16 bit + /// + PCM16 = 128, + + /// + /// Audio format PCM 16 bit + /// + PCM8 = 127, + + /// + /// Audio format G.711 mu-law + /// + G711U = 0, + + /// + /// AudioFormat G.711 A-law + /// + G711A = 8, + + /// + /// No audio format specified + /// + Undefined = -1 + } + + #endregion +} diff --git a/src/libraries/System.Speech/src/Internal/Synthesis/ConvertTextFrag.cs b/src/libraries/System.Speech/src/Internal/Synthesis/ConvertTextFrag.cs new file mode 100644 index 00000000000000..996349ea29ec91 --- /dev/null +++ b/src/libraries/System.Speech/src/Internal/Synthesis/ConvertTextFrag.cs @@ -0,0 +1,440 @@ +// 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.Runtime.InteropServices; +using System.Speech.Synthesis.TtsEngine; + +namespace System.Speech.Internal.Synthesis +{ + internal static class ConvertTextFrag + { + #region internal Methods + + internal static bool ToSapi(List ssmlFrags, ref GCHandle sapiFragLast) + { + bool fFirst = true; + + for (int iFrag = ssmlFrags.Count - 1; iFrag >= 0; iFrag--) + { + TextFragment textFragment = ssmlFrags[iFrag]; + + // Remove the start and end paragraph fragments + if (textFragment.State.Action == TtsEngineAction.StartParagraph || textFragment.State.Action == TtsEngineAction.StartSentence) + { + continue; + } + + SPVTEXTFRAG sapiFrag = new(); + + // start with the text fragment + sapiFrag.gcNext = fFirst ? new GCHandle() : sapiFragLast; + sapiFrag.pNext = fFirst ? IntPtr.Zero : sapiFragLast.AddrOfPinnedObject(); + sapiFrag.gcText = GCHandle.Alloc(textFragment.TextToSpeak, GCHandleType.Pinned); + sapiFrag.pTextStart = sapiFrag.gcText.AddrOfPinnedObject(); + sapiFrag.ulTextSrcOffset = textFragment.TextOffset; + sapiFrag.ulTextLen = textFragment.TextLength; + + // State + SPVSTATE sapiState = new(); + FragmentState ssmlState = textFragment.State; + sapiState.eAction = (SPVACTIONS)ssmlState.Action; + sapiState.LangID = (short)ssmlState.LangId; + sapiState.EmphAdj = ssmlState.Emphasis != 1 ? 0 : 1; + if (ssmlState.Prosody != null) + { + sapiState.RateAdj = SapiRate(ssmlState.Prosody.Rate); + sapiState.Volume = SapiVolume(ssmlState.Prosody.Volume); + sapiState.PitchAdj.MiddleAdj = SapiPitch(ssmlState.Prosody.Pitch); + } + else + { + sapiState.Volume = 100; + } + + sapiState.ePartOfSpeech = SPPARTOFSPEECH.SPPS_Unknown; + + // Set the silence if any + if (sapiState.eAction == SPVACTIONS.SPVA_Silence) + { + sapiState.SilenceMSecs = SapiSilence(ssmlState.Duration, (EmphasisBreak)ssmlState.Emphasis); + } + + // Set the phonemes if any + if (ssmlState.Phoneme != null) + { + sapiState.eAction = SPVACTIONS.SPVA_Pronounce; + sapiFrag.gcPhoneme = GCHandle.Alloc(ssmlState.Phoneme, GCHandleType.Pinned); + sapiState.pPhoneIds = sapiFrag.gcPhoneme.AddrOfPinnedObject(); + + // Get rid of the text if phonemes are defined. This is to be compatible with existing + // TTS engines. + } + else + { + sapiFrag.gcPhoneme = new GCHandle(); + sapiState.pPhoneIds = IntPtr.Zero; + } + + // Set the say-as if any + if (ssmlState.SayAs != null) + { + string format = ssmlState.SayAs.Format; + string interpretAs; + switch (interpretAs = ssmlState.SayAs.InterpretAs) + { + case "spellout": + case "spell-out": + case "characters": + case "letters": + sapiState.eAction = SPVACTIONS.SPVA_SpellOut; + break; + + case "time": + case "date": + if (!string.IsNullOrEmpty(format)) + { + interpretAs = interpretAs + ':' + format; + } + sapiState.Context.pCategory = SapiCategory(sapiFrag, interpretAs, null); + break; + + default: + sapiState.Context.pCategory = SapiCategory(sapiFrag, interpretAs, format); + break; + } + } + + sapiFrag.State = sapiState; + sapiFragLast = GCHandle.Alloc(sapiFrag, GCHandleType.Pinned); + + fFirst = false; + } + return !fFirst; + } + + private static IntPtr SapiCategory(SPVTEXTFRAG sapiFrag, string interpretAs, string format) + { + int posSayAsFormat = Array.BinarySearch(s_asSayAsFormat, interpretAs); + string sFormat = posSayAsFormat >= 0 ? s_asContextFormat[posSayAsFormat] : format; + sapiFrag.gcSayAsCategory = GCHandle.Alloc(sFormat, GCHandleType.Pinned); + return sapiFrag.gcSayAsCategory.AddrOfPinnedObject(); + } + + internal static void FreeTextSegment(ref GCHandle fragment) + { + SPVTEXTFRAG sapiFrag = (SPVTEXTFRAG)fragment.Target; + if (sapiFrag.gcNext.IsAllocated) + { + FreeTextSegment(ref sapiFrag.gcNext); + } + + // free the references to the optional elements + if (sapiFrag.gcPhoneme.IsAllocated) + { + sapiFrag.gcPhoneme.Free(); + } + + if (sapiFrag.gcSayAsCategory.IsAllocated) + { + sapiFrag.gcSayAsCategory.Free(); + } + + // Free the text associated with this fragment + sapiFrag.gcText.Free(); + fragment.Free(); + } + + #endregion + + #region Private Methods + + private static int SapiVolume(ProsodyNumber volume) + { + int sapiVolume = 100; + if (volume.SsmlAttributeId != ProsodyNumber.AbsoluteNumber) + { + switch ((ProsodyVolume)volume.SsmlAttributeId) + { + case ProsodyVolume.ExtraLoud: + sapiVolume = 100; + break; + + case ProsodyVolume.Loud: + sapiVolume = 80; + break; + + case ProsodyVolume.Medium: + sapiVolume = 60; + break; + + case ProsodyVolume.Soft: + sapiVolume = 40; + break; + + case ProsodyVolume.ExtraSoft: + sapiVolume = 20; + break; + + case ProsodyVolume.Silent: + sapiVolume = 0; + break; + } + // add the relative information + sapiVolume = (int)((volume.IsNumberPercent ? sapiVolume * volume.Number : volume.Number) + 0.5); + } + else + { + sapiVolume = (int)(volume.Number + 0.5); + } + + // Check the range. + if (sapiVolume > 100) + { + sapiVolume = 100; + } + if (sapiVolume < 0) + { + sapiVolume = 0; + } + return sapiVolume; + } + + private static int SapiSilence(int duration, EmphasisBreak emphasis) + { + int sapiSilence = 1000; + + if (duration > 0) + { + sapiSilence = duration; + } + else + { + switch (emphasis) + { + // No break, arbitrarily defined as 10 milliseconds + case EmphasisBreak.None: + sapiSilence = 10; + break; + + // Extra small break, arbitrarily defined as 125 milliseconds + case EmphasisBreak.ExtraWeak: + sapiSilence = 125; + break; + + // Small break, arbitrarily defined as 250 milliseconds + case EmphasisBreak.Weak: + sapiSilence = 250; + break; + + // Medium break, arbitrarily defined as 1000 milliseconds + case EmphasisBreak.Medium: + sapiSilence = 1000; + break; + + // Large break, arbitrarily defined as 1750 milliseconds + case EmphasisBreak.Strong: + sapiSilence = 1750; + break; + + // Extra large break, arbitrarily defined as 3000 milliseconds + case EmphasisBreak.ExtraStrong: + sapiSilence = 3000; + break; + } + } + if (sapiSilence < 0 || sapiSilence > 0xffff) + { + sapiSilence = 1000; + } + return sapiSilence; + } + + /// + /// Produces the SAPI "RATE" tag + /// + private static int SapiRate(ProsodyNumber rate) + { + // Okay, we have a RATE element, but what do we set the rate to? + // Rate varies on a scale from -10 to 10 for us. + // There isn't a defined mapping between Words per Minute and rate. + // For percentage changes, we will require that -10 maps to one third the default rate, + // and +10 to three times the default, on a log scale. + // But for absolute or relative (not percent) we can't support this without a defined base-line rate + // We could get away with 180 for this in English, but very variable across languages. + + int sapiRate = 0; + if (rate.SsmlAttributeId != ProsodyNumber.AbsoluteNumber) + { + switch ((ProsodyRate)rate.SsmlAttributeId) + { + case ProsodyRate.ExtraSlow: + sapiRate = -9; + break; + + case ProsodyRate.Slow: + sapiRate = -4; + break; + + case ProsodyRate.Fast: + sapiRate = 4; + break; + + case ProsodyRate.ExtraFast: + sapiRate = 9; + break; + } + + // add the relative information + sapiRate = (int)((rate.IsNumberPercent ? ScaleNumber(rate.Number, sapiRate, 10) : sapiRate) + 0.5); + } + else + { + sapiRate = ScaleNumber(rate.Number, 0, 10); + } + // Check the range. + if (sapiRate > 10) + { + sapiRate = 10; + } + if (sapiRate < -10) + { + sapiRate = -10; + } + return sapiRate; + } + + private static int SapiPitch(ProsodyNumber pitch) + { + int sapiPitch = 0; + + if (pitch.SsmlAttributeId != ProsodyNumber.AbsoluteNumber) + { + switch ((ProsodyPitch)pitch.SsmlAttributeId) + { + case ProsodyPitch.ExtraHigh: + sapiPitch = 9; + break; + + case ProsodyPitch.High: + sapiPitch = 4; + break; + + case ProsodyPitch.Low: + sapiPitch = -4; + break; + + case ProsodyPitch.ExtraLow: + sapiPitch = -9; + break; + } + // add the relative information + sapiPitch = (int)((pitch.IsNumberPercent ? sapiPitch * pitch.Number : pitch.Number) + 0.5); + } + + // Check the range. + if (sapiPitch > 10) + { + sapiPitch = 10; + } + if (sapiPitch < -10) + { + sapiPitch = -10; + } + return sapiPitch; + } + + private static int ScaleNumber(float value, int currentValue, int max) + { + int rate = 0; + // Because we are on a logarithmic scale, can handle percentage changes + // 300% --> multiply by 3.0 --> sapi rate change of +max.0 + // 100% --> multiply by 1.0 --> sapi rate change of 0.0 + // 33% --> multiply by 0.33 --> sapi rate change of -max.0 + if (value >= 0.01) + { + rate = (int)(((Math.Log(value) / Math.Log(3.0)) * max) + 0.5); + rate += currentValue; + if (rate > max) + { + rate = max; + } + else if (rate < -max) + { + rate = -max; + } + } + else + { + rate = -max; + } + return rate; + } + + #endregion + + #region Private Methods + + private static readonly string[] s_asSayAsFormat = new string[] + { + "acronym", + "address", + "cardinal", + "currency", + "date", + "date:d", + "date:dm", + "date:dmy", + "date:m", + "date:md", + "date:mdy", + "date:my", + "date:ym", + "date:ymd", + "date:y", + "digits", + "name", + "net", + "net:email", + "net:uri", + "ordinal", + "spellout", + "telephone", + "time", + "time:hms12", + "time:hms24" + }; + + private static readonly string[] s_asContextFormat = new string[] + { + "name", + "address", + "number_cardinal", + "currency", + "date_md", + "date_dm", + "date_dm", + "date_dmy", + "date_md", + "date_md", + "date_mdy", + "date_my", + "date_ym", + "date_ymd", + "date_year", + "number_digit", + "name", + "web_url", + "E-mail_address", + "web_url", + "number_ordinal", + "", + "phone_number", + "time", + "time", + "time" + }; + + #endregion + } +} diff --git a/src/libraries/System.Speech/src/Internal/Synthesis/EngineSite.cs b/src/libraries/System.Speech/src/Internal/Synthesis/EngineSite.cs new file mode 100644 index 00000000000000..edbdc5deb2e265 --- /dev/null +++ b/src/libraries/System.Speech/src/Internal/Synthesis/EngineSite.cs @@ -0,0 +1,542 @@ +// 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; +using System.Diagnostics; +using System.IO; +using System.Runtime.InteropServices; +using System.Speech.Synthesis; +using System.Speech.Synthesis.TtsEngine; +using System.Text; + +// Exceptions cannot get through the COM code. +// The engine site saves the last exception before sending it back to the client. +#pragma warning disable 6500 + +namespace System.Speech.Internal.Synthesis +{ + internal class EngineSite : ITtsEngineSite, ITtsEventSink + { + #region Constructors + + internal EngineSite(ResourceLoader resourceLoader) + { + _resourceLoader = resourceLoader; + } + + #endregion + + #region Internal Methods + internal TtsEventMapper EventMapper + { + get + { + return _eventMapper; + } + set + { + _eventMapper = value; + } + } + + #region ISpTTSEngineStite implementation + /// + /// Adds events directly to an event sink. + /// + public void AddEvents([MarshalAs(UnmanagedType.LPArray, SizeParamIndex = 1)] SpeechEventInfo[] events, int ulCount) + { + try + { + foreach (SpeechEventInfo sapiEvent in events) + { + int evtMask = 1 << sapiEvent.EventId; + + if (sapiEvent.EventId == (short)TtsEventId.EndInputStream && _eventMapper != null) + { + _eventMapper.FlushEvent(); + } + + if ((evtMask & _eventInterest) != 0) + { + TTSEvent ttsEvent = CreateTtsEvent(sapiEvent); + if (_eventMapper == null) + { + AddEvent(ttsEvent); + } + else + { + _eventMapper.AddEvent(ttsEvent); + } + } + } + } + catch (Exception e) + { + _exception = e; + _actions |= SPVESACTIONS.SPVES_ABORT; + } + } + + /// + /// Queries the voice object to determine which real-time action(s) to perform. + /// + public int Write(IntPtr pBuff, int cb) + { + try + { + _audio.Play(pBuff, cb); + } + catch (Exception e) + { + _exception = e; + _actions |= SPVESACTIONS.SPVES_ABORT; + } + return cb; + } + + /// + /// Retrieves the number and type of items to be skipped in the text stream. + /// + public SkipInfo GetSkipInfo() + { + return new SkipInfo(1 /*BSPVSKIPTYPE.SPVST_SENTENCE */, 1); + } + + /// + /// Notifies that the last skip request has been completed and to pass it the results. + /// + public void CompleteSkip(int ulNumSkipped) + { + return; + } + + /// + /// Passes back the event interest for the voice. + /// + public int EventInterest + { + get + { + return _eventInterest; + } + } + + /// + /// Queries the voice object to determine which real-time action(s) to perform + /// + public int Actions + { + get + { + return (int)_actions; + } + } + + /// + /// Retrieves the current TTS rendering rate adjustment that should be used by the engine. + /// + public int Rate + { + get + { + _actions &= ~SPVESACTIONS.SPVES_RATE; + return _defaultRate; + } + } + + /// + /// Retrieves the base output volume level the engine should use during synthesis. + /// + public int Volume + { + get + { + _actions &= ~SPVESACTIONS.SPVES_VOLUME; + return _volume; + } + } + + /// + /// Load a file either from a local network or from the Internet. + /// + public Stream LoadResource(Uri uri, string mediaType) + { + try + { + string localPath; + string mediaTypeUnused; // TODO: Should this be passed out of this function? + Uri baseUriUnused; + using (Stream stream = _resourceLoader.LoadFile(uri, out mediaTypeUnused, out baseUriUnused, out localPath)) + { + // Read the file in memory for SES and release the original file immediately + // This scheme is really bad if the files being read are big but I would assume + // That it should not be the case. + int cLen = (int)stream.Length; + MemoryStream memStream = new(cLen); + byte[] ab = new byte[cLen]; + stream.Read(ab, 0, ab.Length); + _resourceLoader.UnloadFile(localPath); + memStream.Write(ab, 0, cLen); + memStream.Position = 0; + + return memStream; + } + } + catch (Exception e) + { + _exception = e; + _actions |= SPVESACTIONS.SPVES_ABORT; + } + return null; + } + + #endregion + + public void AddEvent(TTSEvent evt) + { + _audio.InjectEvent(evt); + } + + public void FlushEvent() + { + } + + internal void SetEventsInterest(int eventInterest) + { + _eventInterest = eventInterest; + if (_eventMapper != null) + { + _eventMapper.FlushEvent(); + } + } + + #endregion + + #region Internal Properties + + /// + /// Retrieves the current TTS rendering rate adjustment that should be used by the engine. + /// + internal int VoiceRate + { + get + { + return _defaultRate; + } + set + { + _defaultRate = value; + _actions |= SPVESACTIONS.SPVES_RATE; + } + } + + /// + /// Retrieves the base output volume level the engine should use during synthesis. + /// + internal int VoiceVolume + { + get + { + return _volume; + } + set + { + _volume = value; + _actions |= SPVESACTIONS.SPVES_VOLUME; + } + } + + /// + /// Set and reset the last exception + /// + internal Exception LastException + { + get + { + return _exception; + } + set + { + _exception = value; + } + } + + internal void Abort() + { + _actions = SPVESACTIONS.SPVES_ABORT; + } + + internal void InitRun(AudioBase audioDevice, int defaultRate, Prompt prompt) + { + _audio = audioDevice; + _prompt = prompt; + _defaultRate = defaultRate; + _actions = SPVESACTIONS.SPVES_RATE | SPVESACTIONS.SPVES_VOLUME; + } + + #endregion + + #region Private Members + + private TTSEvent CreateTtsEvent(SpeechEventInfo sapiEvent) + { + TTSEvent ttsEvent; + switch ((TtsEventId)sapiEvent.EventId) + { + case TtsEventId.Phoneme: + ttsEvent = TTSEvent.CreatePhonemeEvent("" + (char)((uint)sapiEvent.Param2 & 0xFFFF), // current phoneme + "" + (char)(sapiEvent.Param1 & 0xFFFF), // next phoneme + TimeSpan.FromMilliseconds(sapiEvent.Param1 >> 16), + (SynthesizerEmphasis)((uint)sapiEvent.Param2 >> 16), + _prompt, _audio.Duration); + break; + case TtsEventId.Bookmark: + // BookmarkDetected + string bookmark = Marshal.PtrToStringUni(sapiEvent.Param2); + ttsEvent = new TTSEvent((TtsEventId)sapiEvent.EventId, _prompt, null, null, _audio.Duration, _audio.Position, bookmark, (uint)sapiEvent.Param1, sapiEvent.Param2); + break; + default: + ttsEvent = new TTSEvent((TtsEventId)sapiEvent.EventId, _prompt, null, null, _audio.Duration, _audio.Position, null, (uint)sapiEvent.Param1, sapiEvent.Param2); + break; + } + return ttsEvent; + } + + #endregion + + #region private Fields + + private int _eventInterest; + + private SPVESACTIONS _actions = SPVESACTIONS.SPVES_RATE | SPVESACTIONS.SPVES_VOLUME; + + private AudioBase _audio; + + private Prompt _prompt; + + // Last Exception + private Exception _exception; + + // Rate setup in the control panel + private int _defaultRate; + + // Rate setup in the control panel + private int _volume = 100; + + // Get a resource load + private ResourceLoader _resourceLoader; + + // Map the TTS events to the right format + private TtsEventMapper _eventMapper; + + #endregion + } + + internal interface ITtsEventSink + { + void AddEvent(TTSEvent evt); + void FlushEvent(); + } + + internal abstract class TtsEventMapper : ITtsEventSink + { + internal TtsEventMapper(ITtsEventSink sink) + { + _sink = sink; + } + + protected virtual void SendToOutput(TTSEvent evt) + { + if (_sink != null) + { + _sink.AddEvent(evt); + } + } + + public virtual void AddEvent(TTSEvent evt) + { + SendToOutput(evt); + } + + public virtual void FlushEvent() + { + if (_sink != null) + { + _sink.FlushEvent(); + } + } + + private ITtsEventSink _sink; + } + + internal class PhonemeEventMapper : TtsEventMapper + { + public enum PhonemeConversion + { + IpaToSapi, SapiToIpa, NoConversion + } + + internal PhonemeEventMapper(ITtsEventSink sink, PhonemeConversion conversion, AlphabetConverter alphabetConverter) : base(sink) + { + _queue = new Queue(); + _phonemeQueue = new Queue(); + _conversion = conversion; + _alphabetConverter = alphabetConverter; + Reset(); + } + + public override void AddEvent(TTSEvent evt) + { + if (_conversion == PhonemeConversion.NoConversion) + { + SendToOutput(evt); + } + else if (evt.Id == TtsEventId.Phoneme) + { + _phonemeQueue.Enqueue(evt); + + int prefixSeek = _phonemes.Length + 1; + _phonemes.Append(evt.Phoneme); + do + { + string prefix = _phonemes.ToString(0, prefixSeek); + if (_alphabetConverter.IsPrefix(prefix, _conversion == PhonemeConversion.SapiToIpa)) + { + if (_alphabetConverter.IsConvertibleUnit(prefix, _conversion == PhonemeConversion.SapiToIpa)) + { + _lastComplete = prefixSeek; + } + prefixSeek++; + } + else + { + if (_lastComplete == 0) + { + Trace.TraceError("Cannot convert the phonemes correctly. Attempt to start over..."); + Reset(); + break; + } + ConvertCompleteUnit(); + _lastComplete = 0; + prefixSeek = 1; + } + } while (prefixSeek <= _phonemes.Length); + } + else + { + SendToQueue(evt); + } + } + + public override void FlushEvent() + { + ConvertCompleteUnit(); + while (_queue.Count > 0) + { + SendToOutput((TTSEvent)_queue.Dequeue()); + } + _phonemeQueue.Clear(); + _lastComplete = 0; + + base.FlushEvent(); + } + + private void ConvertCompleteUnit() + { + if (_lastComplete == 0) + { + return; + } + if (_phonemeQueue.Count == 0) + { + Trace.TraceError("Failed to convert phonemes. Phoneme queue is empty."); + return; + } + + char[] source = new char[_lastComplete]; + _phonemes.CopyTo(0, source, 0, _lastComplete); + _phonemes.Remove(0, _lastComplete); + char[] target; + if (_conversion == PhonemeConversion.IpaToSapi) + { + target = _alphabetConverter.IpaToSapi(source); + } + else + { + target = _alphabetConverter.SapiToIpa(source); + } + + // + // Convert the audio duration + // Update the next phoneme id + // Retain any other information based on the first TTS phoneme event. + // + TTSEvent ttsEvent, targetEvent, basePhonemeEvent = null; + long totalDuration = 0; + basePhonemeEvent = (TTSEvent)_phonemeQueue.Peek(); + for (int i = 0; i < _lastComplete;) + { + ttsEvent = (TTSEvent)_phonemeQueue.Dequeue(); + totalDuration += ttsEvent.PhonemeDuration.Milliseconds; + i += ttsEvent.Phoneme.Length; + } + + targetEvent = TTSEvent.CreatePhonemeEvent(new string(target), "", + TimeSpan.FromMilliseconds(totalDuration), + basePhonemeEvent.PhonemeEmphasis, + basePhonemeEvent.Prompt, + basePhonemeEvent.AudioPosition); + SendToQueue(targetEvent); + } + + private void Reset() + { + _phonemeQueue.Clear(); + _phonemes = new StringBuilder(); + _lastComplete = 0; + } + + private void SendToQueue(TTSEvent evt) + { + if (evt.Id == TtsEventId.Phoneme) + { + TTSEvent firstEvent; + if (_queue.Count > 0) + { + firstEvent = _queue.Dequeue() as TTSEvent; + if (firstEvent.Id == TtsEventId.Phoneme) + { + firstEvent.NextPhoneme = evt.Phoneme; + } + else + { + Trace.TraceError("First event in the queue of the phone mapper is not a PHONEME event"); + } + SendToOutput(firstEvent); + while (_queue.Count > 0) + { + SendToOutput(_queue.Dequeue() as TTSEvent); + } + } + _queue.Enqueue(evt); + } + else + { + if (_queue.Count > 0) + { + _queue.Enqueue(evt); + } + else + { + SendToOutput(evt); + } + } + } + + private PhonemeConversion _conversion; + private StringBuilder _phonemes; + private Queue _queue, _phonemeQueue; + private AlphabetConverter _alphabetConverter; + private int _lastComplete; + } +} diff --git a/src/libraries/System.Speech/src/Internal/Synthesis/EngineSiteSapi.cs b/src/libraries/System.Speech/src/Internal/Synthesis/EngineSiteSapi.cs new file mode 100644 index 00000000000000..d6bf03a57d2f17 --- /dev/null +++ b/src/libraries/System.Speech/src/Internal/Synthesis/EngineSiteSapi.cs @@ -0,0 +1,212 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.IO; +using System.Runtime.InteropServices; +using System.Runtime.InteropServices.ComTypes; +using System.Speech.Internal.SapiInterop; +using System.Speech.Synthesis.TtsEngine; + +#pragma warning disable 56500 // Remove all the catch all statements warnings used by the interop layer + +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 + + /// + /// Adds events directly to an event sink. + /// + 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); + } + + /// + /// Passes back the event interest for the voice. + /// + void ISpEngineSite.GetEventInterest(out long eventInterest) + { + eventInterest = (uint)_site.EventInterest; + } + + /// + /// Queries the voice object to determine which real-time action(s) to perform + /// + [PreserveSig] + int ISpEngineSite.GetActions() + { + return _site.Actions; + } + + /// + /// Queries the voice object to determine which real-time action(s) to perform. + /// + void ISpEngineSite.Write(IntPtr pBuff, int cb, IntPtr pcbWritten) + { + pcbWritten = (IntPtr)_site.Write(pBuff, cb); + } + + /// + /// Retrieves the current TTS rendering rate adjustment that should be used by the engine. + /// + void ISpEngineSite.GetRate(out int pRateAdjust) + { + pRateAdjust = _site.Rate; + } + + /// + /// Retrieves the base output volume level the engine should use during synthesis. + /// + void ISpEngineSite.GetVolume(out short pusVolume) + { + pusVolume = (short)_site.Volume; + } + + /// + /// Retrieves the number and type of items to be skipped in the text stream. + /// + 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; + } + } + + /// + /// Notifies that the last skip request has been completed and to pass it the results. + /// + void ISpEngineSite.CompleteSkip(int ulNumSkipped) + { + _site.CompleteSkip(ulNumSkipped); + } + + /// + /// Load a file either from a local network or from the Internet. + /// + 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 + { + public short EventId; + public short ParameterType; + public int StreamNumber; + public long AudioStreamOffset; + public IntPtr 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) + { + return event1.EventId == event2.EventId && event1.ParameterType == event2.ParameterType && event1.StreamNumber == event2.StreamNumber && event1.AudioStreamOffset == event2.AudioStreamOffset && event1.Param1 == event2.Param1 && event1.Param2 == event2.Param2; + } + public static bool operator !=(SpeechEventSapi event1, SpeechEventSapi event2) + { + return !(event1 == event2); + } + public override bool Equals(object obj) + { + if (!(obj is SpeechEventSapi)) + { + return false; + } + + return this == (SpeechEventSapi)obj; + } + public override int GetHashCode() + { + return base.GetHashCode(); + } + } + + #endregion +} diff --git a/src/libraries/System.Speech/src/Internal/Synthesis/ISSmlParser.cs b/src/libraries/System.Speech/src/Internal/Synthesis/ISSmlParser.cs new file mode 100644 index 00000000000000..4974f04be7693f --- /dev/null +++ b/src/libraries/System.Speech/src/Internal/Synthesis/ISSmlParser.cs @@ -0,0 +1,108 @@ +// 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.Globalization; +using System.Speech.Synthesis; +using System.Speech.Synthesis.TtsEngine; +using System.Xml; + +namespace System.Speech.Internal.Synthesis +{ + #region Internal Types + + internal interface ISsmlParser + { + object ProcessSpeak(string sVersion, string sBaseUri, CultureInfo culture, List extraNamespace); + void ProcessText(string text, object voice, ref FragmentState fragmentState, int position, bool fIgnore); + void ProcessAudio(object voice, string sUri, string baseUri, bool fIgnore); + void ProcessBreak(object voice, ref FragmentState fragmentState, EmphasisBreak eBreak, int time, bool fIgnore); + void ProcessDesc(CultureInfo culture); + void ProcessEmphasis(bool noLevel, EmphasisWord word); + void ProcessMark(object voice, ref FragmentState fragmentState, string name, bool fIgnore); + object ProcessTextBlock(bool isParagraph, object voice, ref FragmentState fragmentState, CultureInfo culture, bool newCulture, VoiceGender gender, VoiceAge age); + void EndProcessTextBlock(bool isParagraph); + void ProcessPhoneme(ref FragmentState fragmentState, AlphabetType alphabet, string ph, char[] phoneIds); + void ProcessProsody(string pitch, string range, string rate, string volume, string duration, string points); + void ProcessSayAs(string interpretAs, string format, string detail); + void ProcessSub(string alias, object voice, ref FragmentState fragmentState, int position, bool fIgnore); + object ProcessVoice(string name, CultureInfo culture, VoiceGender gender, VoiceAge age, int variant, bool fNewCulture, List extraNamespace); + void ProcessLexicon(Uri uri, string type); + void EndElement(); + void EndSpeakElement(); + + void ProcessUnknownElement(object voice, ref FragmentState fragmentState, XmlReader reader); + void StartProcessUnknownAttributes(object voice, ref FragmentState fragmentState, string element, List extraAttributes); + void EndProcessUnknownAttributes(object voice, ref FragmentState fragmentState, string element, List extraAttributes); + + // Prompt data used + void ContainsPexml(string pexmlPrefix); + + // Prompt Engine tags + bool BeginPromptEngineOutput(object voice); + void EndPromptEngineOutput(object voice); + + // global elements + bool ProcessPromptEngineDatabase(object voice, string fname, string delta, string idset); + bool ProcessPromptEngineDiv(object voice); + bool ProcessPromptEngineId(object voice, string id); + + // scoped elements + bool BeginPromptEngineTts(object voice); + void EndPromptEngineTts(object voice); + bool BeginPromptEngineWithTag(object voice, string tag); + void EndPromptEngineWithTag(object voice, string tag); + bool BeginPromptEngineRule(object voice, string name); + void EndPromptEngineRule(object voice, string name); + + // Properties + string Ssml { get; } + } + + internal class LexiconEntry + { + internal Uri _uri; + internal string _mediaType; + + internal LexiconEntry(Uri uri, string mediaType) + { + _uri = uri; + _mediaType = mediaType; + } + + /// + /// Tests whether two objects are equivalent + /// + public override bool Equals(object obj) + { + LexiconEntry entry = obj as LexiconEntry; + return entry != null && _uri.Equals(entry._uri); + } + + /// + /// Overrides Object.GetHashCode() + /// + public override int GetHashCode() + { + return _uri.GetHashCode(); + } + } + + internal class SsmlXmlAttribute + { + internal SsmlXmlAttribute(string prefix, string name, string value, string ns) + { + _prefix = prefix; + _name = name; + _value = value; + _ns = ns; + } + + internal string _prefix; + internal string _name; + internal string _value; + internal string _ns; + } + + #endregion +} diff --git a/src/libraries/System.Speech/src/Internal/Synthesis/PcmConverter.cs b/src/libraries/System.Speech/src/Internal/Synthesis/PcmConverter.cs new file mode 100644 index 00000000000000..d86b8bc8cb7673 --- /dev/null +++ b/src/libraries/System.Speech/src/Internal/Synthesis/PcmConverter.cs @@ -0,0 +1,466 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace System.Speech.Internal.Synthesis +{ + internal class PcmConverter + { + #region Internal Methods + + /// + /// Description: + /// first read samples into VAPI_PCM16, then judge cases : + /// 1. STEREO -> mono + resampling + /// STEREO -> 1 mono -> reSampling + /// 2. mono -> STEREO + resampling + /// mono -> reSampling -> STEREO + /// 3. STEREO -> STEREO + resampling + /// STEREO -> 2 MONO - > reSampling -> 2 MONO -> STEREO + /// 4. mono -> mono + resampling + /// mono -> reSampling -> mono + /// + internal bool PrepareConverter(ref WAVEFORMATEX inWavFormat, ref WAVEFORMATEX outWavFormat) + { + bool convert = true; + // Check if we can deal with the format + if (!(inWavFormat.nSamplesPerSec > 0 && inWavFormat.nChannels <= 2 && inWavFormat.nChannels > 0 && outWavFormat.nChannels > 0 && outWavFormat.nSamplesPerSec > 0 && outWavFormat.nChannels <= 2)) + { + throw new FormatException(); + } + + _iInFormatType = AudioFormatConverter.TypeOf(inWavFormat); + _iOutFormatType = AudioFormatConverter.TypeOf(outWavFormat); + if (_iInFormatType < 0 || _iOutFormatType < 0) + { + throw new FormatException(); + } + + // Check if Format in == Format out + if (outWavFormat.nSamplesPerSec == inWavFormat.nSamplesPerSec && _iOutFormatType == _iInFormatType && outWavFormat.nChannels == inWavFormat.nChannels) + { + convert = false; + } + else + { + //--- need reset filter + if (inWavFormat.nSamplesPerSec != outWavFormat.nSamplesPerSec) + { + CreateResamplingFilter(inWavFormat.nSamplesPerSec, outWavFormat.nSamplesPerSec); + } + + // Keep a reference to the WaveHeaderformat + _inWavFormat = inWavFormat; + _outWavFormat = outWavFormat; + } + return convert; + } + + /// + /// Description: + /// first read samples into VAPI_PCM16, then judge cases : + /// 1. STEREO -> mono + resampling + /// STEREO -> 1 mono -> reSampling + /// 2. mono -> STEREO + resampling + /// mono -> reSampling -> STEREO + /// 3. STEREO -> STEREO + resampling + /// STEREO -> 2 MONO - > reSampling -> 2 MONO -> STEREO + /// 4. mono -> mono + resampling + /// mono -> reSampling -> mono + /// + internal byte[] ConvertSamples(byte[] pvInSamples) + { + short[] pnBuff = null; + + //--- Convert samples to VAPI_PCM16 + short[] inSamples = AudioFormatConverter.Convert(pvInSamples, _iInFormatType, AudioCodec.PCM16); + + //--- case 1 + if (_inWavFormat.nChannels == 2 && _outWavFormat.nChannels == 1) + { + pnBuff = Resample(_inWavFormat, _outWavFormat, Stereo2Mono(inSamples), _leftMemory); + } + + //--- case 2 + else if (_inWavFormat.nChannels == 1 && _outWavFormat.nChannels == 2) + { + //--- resampling + pnBuff = Mono2Stereo(Resample(_inWavFormat, _outWavFormat, inSamples, _leftMemory)); + } + + //--- case 3 + if (_inWavFormat.nChannels == 2 && _outWavFormat.nChannels == 2) + { + if (_inWavFormat.nSamplesPerSec != _outWavFormat.nSamplesPerSec) + { + short[] leftChannel; + short[] rightChannel; + SplitStereo(inSamples, out leftChannel, out rightChannel); + pnBuff = MergeStereo(Resample(_inWavFormat, _outWavFormat, leftChannel, _leftMemory), Resample(_inWavFormat, _outWavFormat, rightChannel, _rightMemory)); + } + else + { + pnBuff = inSamples; + } + } + + //--- case 4 + if (_inWavFormat.nChannels == 1 && _outWavFormat.nChannels == 1) + { + pnBuff = Resample(_inWavFormat, _outWavFormat, inSamples, _leftMemory); + } + + _eChunkStatus = Block.Middle; + //---Convert samples to output format + return AudioFormatConverter.Convert(pnBuff, AudioCodec.PCM16, _iOutFormatType); + } + + #endregion + + #region private Fields + + /// + /// Convert the data from one sample rate to an another + /// + private short[] Resample(WAVEFORMATEX inWavFormat, WAVEFORMATEX outWavFormat, short[] pnBuff, float[] memory) + { + if (inWavFormat.nSamplesPerSec != outWavFormat.nSamplesPerSec) + { + float[] pdBuff = Short2Float(pnBuff); + + //--- resample + pdBuff = Resampling(pdBuff, memory); + + pnBuff = Float2Short(pdBuff); + } + return pnBuff; + } + + /// + /// convert short array to float array + /// + private static float[] Short2Float(short[] inSamples) + { + float[] pdOut = new float[inSamples.Length]; + + for (int i = 0; i < inSamples.Length; i++) + { + pdOut[i] = inSamples[i]; + } + + return pdOut; + } + + /// + /// convert float array to short array + /// + private static short[] Float2Short(float[] inSamples) + { + short[] outSamples = new short[inSamples.Length]; + float dtmp; + + for (int i = 0; i < inSamples.Length; i++) + { + if (inSamples[i] >= 0) + { + dtmp = inSamples[i] + 0.5f; + if (dtmp > short.MaxValue) + { + dtmp = short.MaxValue; + } + } + else + { + dtmp = inSamples[i] - 0.5f; + if (dtmp < short.MinValue) + { + dtmp = short.MinValue; + } + } + outSamples[i] = (short)(dtmp); + } + return outSamples; + } + + /// + /// convert mono speech to stereo speech + /// + private static short[] Mono2Stereo(short[] inSamples) + { + short[] outSamples = new short[inSamples.Length * 2]; + + for (int i = 0, k = 0; i < inSamples.Length; i++, k += 2) + { + outSamples[k] = inSamples[i]; + outSamples[k + 1] = inSamples[i]; + } + + return outSamples; + } + + /// + /// convert stereo speech to mono speech + /// + private static short[] Stereo2Mono(short[] inSamples) + { + short[] outSamples = new short[inSamples.Length / 2]; + + for (int i = 0, k = 0; i < inSamples.Length; i += 2, k++) + { + outSamples[k] = unchecked((short)((inSamples[i] + inSamples[i + 1]) / 2)); + } + + return outSamples; + } + + /// + /// merge 2 channel signals into one signal + /// + private static short[] MergeStereo(short[] leftSamples, short[] rightSamples) + { + short[] outSamples = new short[leftSamples.Length * 2]; + + for (int i = 0, k = 0; i < leftSamples.Length; i++, k += 2) + { + outSamples[k] = leftSamples[i]; + outSamples[k + 1] = rightSamples[i]; + } + + return outSamples; + } + + /// + /// split stereo signals into 2 channel mono signals + /// + private static void SplitStereo(short[] inSamples, out short[] leftSamples, out short[] rightSamples) + { + int length = inSamples.Length / 2; + + leftSamples = new short[length]; + rightSamples = new short[length]; + + for (int i = 0, k = 0; i < inSamples.Length; i += 2) + { + leftSamples[k] = inSamples[i]; + rightSamples[k] = inSamples[i + 1]; + } + } + + private void CreateResamplingFilter(int inHz, int outHz) + { + int iLimitFactor; + + if (inHz <= 0) + { + throw new ArgumentOutOfRangeException(nameof(inHz)); + } + + if (outHz <= 0) + { + throw new ArgumentOutOfRangeException(nameof(outHz)); + } + + FindResampleFactors(inHz, outHz); + iLimitFactor = (_iUpFactor > _iDownFactor) ? _iUpFactor : _iDownFactor; + + _iFilterHalf = (int)(inHz * iLimitFactor * _dHalfFilterLen); + _iFilterLen = 2 * _iFilterHalf + 1; + + _filterCoeff = WindowedLowPass(.5f / iLimitFactor, _iUpFactor); + + _iBuffLen = (int)(_iFilterLen / (float)_iUpFactor); + + _leftMemory = new float[_iBuffLen]; + _rightMemory = new float[_iBuffLen]; + + _eChunkStatus = Block.First; // first chunk + } + + /// + /// Creates a low pass filter using the windowing method. + /// dCutOff is spec. in normalized frequency + /// + private float[] WindowedLowPass(float dCutOff, float dGain) + { + float[] pdCoeffs = null; + float[] pdWindow = null; + double dArg; + double dSinc; + + System.Diagnostics.Debug.Assert(dCutOff > 0.0 && dCutOff < 0.5); + + pdWindow = Blackman(_iFilterLen, true); + + pdCoeffs = new float[_iFilterLen]; + + dArg = 2.0f * Math.PI * dCutOff; + pdCoeffs[_iFilterHalf] = (float)(dGain * 2.0 * dCutOff); + + for (long i = 1; i <= _iFilterHalf; i++) + { + dSinc = dGain * Math.Sin(dArg * i) / (Math.PI * i) * pdWindow[_iFilterHalf - i]; + pdCoeffs[_iFilterHalf + i] = (float)dSinc; + pdCoeffs[_iFilterHalf - i] = (float)dSinc; + } + + return pdCoeffs; + } + + private void FindResampleFactors(int inHz, int outHz) + { + int iDiv = 1; + int i; + + while (iDiv != 0) + { + iDiv = 0; + for (i = 0; i < s_piPrimes.Length; i++) + { + if ((inHz % s_piPrimes[i]) == 0 && (outHz % s_piPrimes[i]) == 0) + { + inHz /= s_piPrimes[i]; + outHz /= s_piPrimes[i]; + iDiv = 1; + break; + } + } + } + + _iUpFactor = outHz; + _iDownFactor = inHz; + } + + private float[] Resampling(float[] inSamples, float[] pdMemory) + { + int cInSamples = inSamples.Length; + int cOutSamples; + int iPhase; + int j; + int n; + int iAddHalf; + + if (_eChunkStatus == Block.First) + { + cOutSamples = (cInSamples * _iUpFactor - _iFilterHalf) / _iDownFactor; + iAddHalf = 1; + } + else if (_eChunkStatus == Block.Middle) + { + cOutSamples = (cInSamples * _iUpFactor) / _iDownFactor; + iAddHalf = 2; + } + else + { + System.Diagnostics.Debug.Assert(_eChunkStatus == Block.Last); + cOutSamples = (_iFilterHalf * _iUpFactor) / _iDownFactor; + iAddHalf = 2; + } + + if (cOutSamples < 0) + { + cOutSamples = 0; + } + float[] outSamples = new float[cOutSamples]; + + for (int i = 0; i < cOutSamples; i++) + { + double dAcum = 0.0; + + n = ((i * _iDownFactor - iAddHalf * _iFilterHalf) / _iUpFactor); + iPhase = (i * _iDownFactor) - (n * _iUpFactor + iAddHalf * _iFilterHalf); + + for (j = 0; j < _iFilterLen / _iUpFactor; j++) + { + if (_iUpFactor * j > iPhase) + { + if (n + j >= 0 && n + j < cInSamples) + { + dAcum += inSamples[n + j] * _filterCoeff[_iUpFactor * j - iPhase]; + } + else if (n + j < 0) + { + dAcum += pdMemory[_iBuffLen + n + j] * _filterCoeff[_iUpFactor * j - iPhase]; + } + } + } + + outSamples[i] = (float)dAcum; + } + + //--- store samples into buffer + if (_eChunkStatus != Block.Last) + { + n = cInSamples - (_iBuffLen + 1); + for (int i = 0; i < _iBuffLen; i++) + { + if (n >= 0) + { + pdMemory[i] = inSamples[n++]; + } + else + { + n++; + pdMemory[i] = 0.0f; + } + } + } + + return outSamples; + } + + /// + /// Returns a vector with a Blackman window of the specified length. + /// + private static float[] Blackman(int iLength, bool bSymmetric) + { + float[] pdWindow = new float[iLength]; + double dArg, dArg2; + + dArg = 2.0 * Math.PI; + if (bSymmetric) + { + dArg /= (float)(iLength - 1); + } + else + { + dArg /= (float)iLength; + } + + dArg2 = 2.0 * dArg; + + for (int i = 0; i < iLength; i++) + { + pdWindow[i] = (float)(0.42 - (0.5 * Math.Cos(dArg * i)) + (0.08 * Math.Cos(dArg2 * i))); + } + + return pdWindow; + } + + #endregion + + #region private Fields + + private enum Block { First, Middle, Last }; + + private WAVEFORMATEX _inWavFormat; + private WAVEFORMATEX _outWavFormat; + private AudioCodec _iInFormatType; + private AudioCodec _iOutFormatType; + + private Block _eChunkStatus; + private int _iUpFactor; + private int _iFilterHalf; + private int _iDownFactor; + private int _iFilterLen; + private int _iBuffLen; + private float[] _filterCoeff; + + private float[] _leftMemory; + private float[] _rightMemory; + + private const float _dHalfFilterLen = 0.0005f; + + private static readonly int[] s_piPrimes = new int[] { 2, 3, 5, 7, 11, 13, 17, 19, 23, 29, 31, 37 }; + + #endregion + } +} diff --git a/src/libraries/System.Speech/src/Internal/Synthesis/SSmlParser.cs b/src/libraries/System.Speech/src/Internal/Synthesis/SSmlParser.cs new file mode 100644 index 00000000000000..0ed306da8597eb --- /dev/null +++ b/src/libraries/System.Speech/src/Internal/Synthesis/SSmlParser.cs @@ -0,0 +1,2154 @@ +// 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.Globalization; +using System.IO; +using System.Net; +using System.Speech.Synthesis; +using System.Speech.Synthesis.TtsEngine; +using System.Text; +using System.Xml; + +namespace System.Speech.Internal.Synthesis +{ + internal static class SsmlParser + { + #region Internal Methods + + /// + /// Parse an SSML stream and build a set of SSML Text Fragments + /// + internal static void Parse(string ssml, ISsmlParser engine, object voice) + { + // Remove the CR and LF + string ssmlNoCrLf = ssml.Replace('\n', ' '); + ssmlNoCrLf = ssmlNoCrLf.Replace('\r', ' '); + XmlTextReader reader = new(new StringReader(ssmlNoCrLf)); + + // Parse the stream + Parse(reader, engine, voice); + } + + /// + /// Parse an SSML stream and build a set of SSML Text Fragments + /// + internal static void Parse(XmlReader reader, ISsmlParser engine, object voice) + { + try + { + bool isSpeakElementFound = false; + + while (reader.Read()) + { + // Ignore XmlDeclaration, ProcessingInstruction, Comment, DocumentType, Entity, Notation. + if ((reader.NodeType == XmlNodeType.Element) && (reader.LocalName == "speak")) + { + // SSML documents must start with the "speak" element + if (isSpeakElementFound) + { + ThrowFormatException(SRID.GrammarDefTwice); + } + else + { + // The XML header is read, real work starts here + ProcessSpeakElement(reader, engine, voice); + isSpeakElementFound = true; + } + } + } + + if (!isSpeakElementFound) + { + ThrowFormatException(SRID.SynthesizerNoSpeak); + } + } + catch (XmlException eXml) + { + throw new FormatException(SR.Get(SRID.InvalidXml), eXml); + } + } + + #endregion + + #region Private Methods + + /// + /// Validate the Speak element + /// + private static void ProcessSpeakElement(XmlReader reader, ISsmlParser engine, object voice) + { + SsmlAttributes ssmlAttributes = new(); + ssmlAttributes._voice = voice; + ssmlAttributes._age = VoiceAge.NotSet; + ssmlAttributes._gender = VoiceGender.NotSet; + ssmlAttributes._unknownNamespaces = new List(); + + string sVersion = null; + string sCulture = null; + string sBaseUri = null; + CultureInfo culture = null; + List extraSpeakAttributes = new(); + Exception innerException = null; + + // Process attributes. + while (reader.MoveToNextAttribute()) + { + bool isInvalidAttribute = false; + + // emptyNamespace + if (reader.NamespaceURI.Length == 0) + { + switch (reader.LocalName) + { + case "version": + CheckForDuplicates(ref sVersion, reader); + if (sVersion != "1.0") + { + ThrowFormatException(SRID.InvalidVersion); + } + break; + + default: + isInvalidAttribute = true; + break; + } + } + else if (reader.NamespaceURI == xmlNamespace) + { + switch (reader.LocalName) + { + case "lang": + CheckForDuplicates(ref sCulture, reader); + try + { + culture = new CultureInfo(sCulture); + } + catch (ArgumentException e) + { + innerException = e; + // Unknown Culture info, fall back to the base culture. + int pos = reader.Value.IndexOf("-", StringComparison.Ordinal); + if (pos > 0) + { + try + { + culture = new CultureInfo(reader.Value.Substring(0, pos)); + } + catch (ArgumentException) + { + isInvalidAttribute = true; + } + } + else + { + isInvalidAttribute = true; + } + } + break; + + case "base": + CheckForDuplicates(ref sBaseUri, reader); + break; + + default: + isInvalidAttribute = true; + break; + } + } + else if (reader.NamespaceURI == xmlNamespaceXmlns) + { + if (reader.Value != xmlNamespaceSsml && reader.Value != xmlNamespacePrompt) + { + ssmlAttributes._unknownNamespaces.Add(new SsmlXmlAttribute(reader.Prefix, reader.LocalName, reader.Value, reader.NamespaceURI)); + } + else if (reader.Value == xmlNamespacePrompt) + { + engine.ContainsPexml(reader.LocalName); + } + } + else + { + extraSpeakAttributes.Add(new SsmlXmlAttribute(reader.Prefix, reader.LocalName, reader.Value, reader.NamespaceURI)); + } + + if (isInvalidAttribute) + { + ThrowFormatException(innerException, SRID.InvalidElement, reader.Name); + } + } + + if (string.IsNullOrEmpty(sVersion)) + { + ThrowFormatException(SRID.MissingRequiredAttribute, "version", "speak"); + } + + if (string.IsNullOrEmpty(sCulture)) + { + ThrowFormatException(SRID.MissingRequiredAttribute, "lang", "speak"); + } + + // append the local attributes to list of unknown attributes + List extraAttributes = null; + foreach (SsmlXmlAttribute attribute in extraSpeakAttributes) + { + ssmlAttributes.AddUnknowAttribute(attribute, ref extraAttributes); + } + + voice = engine.ProcessSpeak(sVersion, sBaseUri, culture, ssmlAttributes._unknownNamespaces); + + ssmlAttributes._fragmentState.LangId = culture.LCID; + ssmlAttributes._voice = voice; + ssmlAttributes._baseUri = sBaseUri; + + // Process child elements. + SsmlElement possibleChild = SsmlElement.Lexicon | SsmlElement.Meta | SsmlElement.MetaData | SsmlElement.ParagraphOrSentence | SsmlElement.AudioMarkTextWithStyle | ElementPromptEngine(ssmlAttributes); + ProcessElement(reader, engine, "speak", possibleChild, ssmlAttributes, false, extraAttributes); + + // Notify the engine that the element is processed + engine.EndSpeakElement(); + } + + /// + /// Generic method to process an SSML element. + /// The element name is fetch from the element name array and + /// the delegate for that element will be called. + /// + private static void ProcessElement(XmlReader reader, ISsmlParser engine, string sElement, SsmlElement possibleElements, SsmlAttributes ssmAttributesParent, bool fIgnore, List extraAttributes) + { + // Make a local copy of the ssmlAttribute + SsmlAttributes ssmlAttributes = new(); + + // This is equivalent to a memcpy + ssmlAttributes = ssmAttributesParent; + + // Flush any remaining attributes from the previous element list + if (extraAttributes != null && extraAttributes.Count > 0) + { + engine.StartProcessUnknownAttributes(ssmlAttributes._voice, ref ssmlAttributes._fragmentState, sElement, extraAttributes); + } + + // Move to containing element of attributes + reader.MoveToElement(); + if (!reader.IsEmptyElement) + { + // Process each child element while not at end element + reader.Read(); + do + { + switch (reader.NodeType) + { + case XmlNodeType.Element: + int iElement = Array.BinarySearch(s_elementsName, reader.LocalName); + if (iElement >= 0) + { + s_parseElements[iElement](reader, engine, possibleElements, ssmlAttributes, fIgnore); + } + else + { + // Could be an element from some undefined namespace + if (!ssmlAttributes.IsOtherNamespaceElement(reader)) + { + ThrowFormatException(SRID.InvalidElement, reader.Name); + } + else + { + engine.ProcessUnknownElement(ssmlAttributes._voice, ref ssmlAttributes._fragmentState, reader); + continue; + } + } + reader.Read(); + break; + + case XmlNodeType.Text: + if ((possibleElements & SsmlElement.Text) != 0) + { + engine.ProcessText(reader.Value, ssmlAttributes._voice, ref ssmlAttributes._fragmentState, GetColumnPosition(reader), fIgnore); + } + else + { + ThrowFormatException(SRID.InvalidElement, reader.Name); + } + reader.Read(); + break; + + case XmlNodeType.EndElement: + break; + + default: + reader.Read(); + break; + } + } + while (reader.NodeType != XmlNodeType.EndElement && reader.NodeType != XmlNodeType.None); + } + + // Flush any remaining attributes from the previous element list + if (extraAttributes != null && extraAttributes.Count > 0) + { + engine.EndProcessUnknownAttributes(ssmlAttributes._voice, ref ssmlAttributes._fragmentState, sElement, extraAttributes); + } + } + + private static void ParseAudio(XmlReader reader, ISsmlParser engine, SsmlElement element, SsmlAttributes ssmAttributesParent, bool fIgnore) + { + // Validate the SSML markup + string sElement = ValidateElement(element, SsmlElement.Audio, reader.Name); + + // Make a local copy of the ssmlAttribute + SsmlAttributes ssmlAttributes = new(); + List extraAttributes = null; + + // This is equivalent to a memcpy + ssmlAttributes = ssmAttributesParent; + + string sUri = null; + bool fRenderDesc = false; + while (reader.MoveToNextAttribute()) + { + // Namespace must be empty + bool isInvalidAttribute = reader.NamespaceURI.Length != 0; + + if (!isInvalidAttribute) + { + switch (reader.LocalName) + { + case "src": + CheckForDuplicates(ref sUri, reader); + // Audio element + try + { + engine.ProcessAudio(ssmlAttributes._voice, sUri, ssmlAttributes._baseUri, fIgnore); + } + catch (IOException) + { + fRenderDesc = true; + } + catch (WebException) + { + fRenderDesc = true; + } + break; + + default: + isInvalidAttribute = true; + break; + } + } + if (isInvalidAttribute && !ssmlAttributes.AddUnknowAttribute(reader, ref extraAttributes)) + { + ThrowFormatException(SRID.InvalidItemAttribute, reader.Name); + } + } + + ssmlAttributes._fRenderDesc = fRenderDesc; + + // Process child elements. + SsmlElement possibleChild = SsmlElement.Desc | SsmlElement.ParagraphOrSentence | SsmlElement.AudioMarkTextWithStyle | ElementPromptEngine(ssmlAttributes); + ProcessElement(reader, engine, sElement, possibleChild, ssmlAttributes, !fRenderDesc, extraAttributes); + + // Notify the engine that the element is processed + engine.EndElement(); + } + + private static void ParseBreak(XmlReader reader, ISsmlParser engine, SsmlElement element, SsmlAttributes ssmAttributesParent, bool fIgnore) + { + // Validate the SSML markup + string sElement = ValidateElement(element, SsmlElement.Break, reader.Name); + + // Make a local copy of the ssmlAttribute + SsmlAttributes ssmlAttributes = new(); + List extraAttributes = null; + + // This is equivalent to a memcpy + ssmlAttributes = ssmAttributesParent; + ssmlAttributes._fragmentState.Action = TtsEngineAction.Silence; + ssmlAttributes._fragmentState.Emphasis = (int)EmphasisBreak.Default; + + string sTime = null; + string sStrength = null; + while (reader.MoveToNextAttribute()) + { + // Namespace must be empty + bool isInvalidAttribute = reader.NamespaceURI.Length != 0; + + if (!isInvalidAttribute) + { + switch (reader.LocalName) + { + case "time": + { + CheckForDuplicates(ref sTime, reader); + ssmlAttributes._fragmentState.Emphasis = (int)EmphasisBreak.None; + ssmlAttributes._fragmentState.Duration = ParseCSS2Time(sTime); + isInvalidAttribute = ssmlAttributes._fragmentState.Duration < 0; + } + break; + + case "strength": + CheckForDuplicates(ref sStrength, reader); + if (sTime == null) + { + ssmlAttributes._fragmentState.Duration = 0; + int pos = Array.BinarySearch(s_breakStrength, sStrength); + if (pos < 0) + { + isInvalidAttribute = true; + } + else + { + // SSML Spec if both strength and time are supplied, ignore strength + if (ssmlAttributes._fragmentState.Emphasis != (int)EmphasisBreak.None) + { + ssmlAttributes._fragmentState.Emphasis = (int)s_breakEmphasis[pos]; + } + } + } + break; + + default: + isInvalidAttribute = true; + break; + } + } + if (isInvalidAttribute && !ssmlAttributes.AddUnknowAttribute(reader, ref extraAttributes)) + { + ThrowFormatException(SRID.InvalidSpeakAttribute, reader.Name, "break"); + } + } + + engine.ProcessBreak(ssmlAttributes._voice, ref ssmlAttributes._fragmentState, (EmphasisBreak)ssmlAttributes._fragmentState.Emphasis, ssmlAttributes._fragmentState.Duration, fIgnore); + + // No Children allowed . + ProcessElement(reader, engine, sElement, 0, ssmlAttributes, true, extraAttributes); + + // Notify the engine that the element is processed + engine.EndElement(); + } + + private static void ParseDesc(XmlReader reader, ISsmlParser engine, SsmlElement element, SsmlAttributes ssmAttributesParent, bool fIgnore) + { + // Validate the SSML markup + string sElement = ValidateElement(element, SsmlElement.Desc, reader.Name); + + // Make a local copy of the ssmlAttribute + SsmlAttributes ssmlAttributes = new(); + List extraAttributes = null; + + // This is equivalent to a memcpy + ssmlAttributes = ssmAttributesParent; + + string sCulture = null; + CultureInfo culture = null; + while (reader.MoveToNextAttribute()) + { + bool isInvalidAttribute = reader.NamespaceURI != xmlNamespace; + + if (!isInvalidAttribute) + { + switch (reader.LocalName) + { + // The W3C spec says ignore + case "lang": + CheckForDuplicates(ref sCulture, reader); + try + { + culture = new CultureInfo(sCulture); + } + catch (ArgumentException) + { + isInvalidAttribute = true; + } + isInvalidAttribute &= culture != null; + break; + + default: + isInvalidAttribute = true; + break; + } + } + if (isInvalidAttribute && !ssmlAttributes.AddUnknowAttribute(reader, ref extraAttributes)) + { + ThrowFormatException(SRID.InvalidItemAttribute, reader.Name); + } + } + + engine.ProcessDesc(culture); + + // Process child elements. + ProcessElement(reader, engine, sElement, SsmlElement.Text, ssmlAttributes, true, extraAttributes); + + // Notify the engine that the element is processed + engine.EndElement(); + } + + private static void ParseEmphasis(XmlReader reader, ISsmlParser engine, SsmlElement element, SsmlAttributes ssmAttributesParent, bool fIgnore) + { + // Validate the SSML markup + string sElement = ValidateElement(element, SsmlElement.Emphasis, reader.Name); + + // Make a local copy of the ssmlAttribute + SsmlAttributes ssmlAttributes = new(); + List extraAttributes = null; + + // This is equivalent to a memcpy + ssmlAttributes = ssmAttributesParent; + + // Set the default value + ssmlAttributes._fragmentState.Emphasis = (int)EmphasisWord.Moderate; + + string sLevel = null; + while (reader.MoveToNextAttribute()) + { + // Namespace must be empty + bool isInvalidAttribute = reader.NamespaceURI.Length != 0; + + if (!isInvalidAttribute) + { + switch (reader.LocalName) + { + // The W3C spec says ignore + case "level": + CheckForDuplicates(ref sLevel, reader); + int pos = Array.BinarySearch(s_emphasisNames, sLevel); + if (pos < 0) + { + isInvalidAttribute = true; + } + else + { + ssmlAttributes._fragmentState.Emphasis = (int)s_emphasisWord[pos]; + } + break; + + default: + isInvalidAttribute = true; + break; + } + } + if (isInvalidAttribute && !ssmlAttributes.AddUnknowAttribute(reader, ref extraAttributes)) + { + ThrowFormatException(SRID.InvalidItemAttribute, reader.Name); + } + } + + engine.ProcessEmphasis(!string.IsNullOrEmpty(sLevel), (EmphasisWord)ssmlAttributes._fragmentState.Emphasis); + + // Process child elements. + SsmlElement possibleChild = SsmlElement.AudioMarkTextWithStyle | ElementPromptEngine(ssmlAttributes); + ProcessElement(reader, engine, sElement, possibleChild, ssmlAttributes, fIgnore, extraAttributes); + + // Notify the engine that the element is processed + engine.EndElement(); + } + + private static void ParseMark(XmlReader reader, ISsmlParser engine, SsmlElement element, SsmlAttributes ssmAttributesParent, bool fIgnore) + { + // Validate the SSML markup + string sElement = ValidateElement(element, SsmlElement.Mark, reader.Name); + + // Make a local copy of the ssmlAttribute + SsmlAttributes ssmlAttributes = new(); + List extraAttributes = null; + + // This is equivalent to a memcpy + ssmlAttributes = ssmAttributesParent; + + string sName = null; + while (reader.MoveToNextAttribute()) + { + // Namespace must be empty + bool isInvalidAttribute = reader.NamespaceURI.Length != 0; + + if (!isInvalidAttribute) + { + switch (reader.LocalName) + { + // The W3C spec says ignore + case "name": + CheckForDuplicates(ref sName, reader); + break; + + default: + isInvalidAttribute = true; + break; + } + } + if (isInvalidAttribute && !ssmlAttributes.AddUnknowAttribute(reader, ref extraAttributes)) + { + ThrowFormatException(SRID.InvalidItemAttribute, reader.Name); + } + } + + if (string.IsNullOrEmpty(sName)) + { + ThrowFormatException(SRID.MissingRequiredAttribute, "name", "mark"); + } + + ssmlAttributes._fragmentState.Action = TtsEngineAction.Bookmark; + engine.ProcessMark(ssmlAttributes._voice, ref ssmlAttributes._fragmentState, sName, fIgnore); + + // No Children allowed. + ProcessElement(reader, engine, sElement, 0, ssmlAttributes, true, extraAttributes); + + // Notify the engine that the element is processed + engine.EndElement(); + } + + private static void ParseMetaData(XmlReader reader, ISsmlParser engine, SsmlElement element, SsmlAttributes ssmAttributesParent, bool fIgnore) + { + // Validate the SSML markup + ValidateElement(element, SsmlElement.MetaData, reader.Name); + + // No processing for this element, skip + if (!reader.IsEmptyElement) + { + int cEndNode = 1; + do + { + // Loop until we reach the end of the metadata element + reader.Read(); + + // Count the number of elements processed + if (reader.NodeType == XmlNodeType.Element) + { + cEndNode++; + } + if (reader.NodeType == XmlNodeType.EndElement || reader.NodeType == XmlNodeType.None) + { + cEndNode--; + } + } + while (cEndNode > 0); + + // Consume the end element + System.Diagnostics.Debug.Assert(reader.NodeType == XmlNodeType.EndElement); + } + } + + private static void ParseParagraph(XmlReader reader, ISsmlParser engine, SsmlElement element, SsmlAttributes ssmAttributesParent, bool fIgnore) + { + // Validate the SSML markup + string sElement = ValidateElement(element, SsmlElement.Paragraph, reader.Name); + + ParseTextBlock(reader, engine, true, sElement, ssmAttributesParent, fIgnore); + } + + private static void ParseSentence(XmlReader reader, ISsmlParser engine, SsmlElement element, SsmlAttributes ssmAttributesParent, bool fIgnore) + { + // Validate the SSML markup + string sElement = ValidateElement(element, SsmlElement.Sentence, reader.Name); + + ParseTextBlock(reader, engine, false, sElement, ssmAttributesParent, fIgnore); + } + + private static void ParseTextBlock(XmlReader reader, ISsmlParser engine, bool isParagraph, string sElement, SsmlAttributes ssmAttributesParent, bool fIgnore) + { + // Make a local copy of the ssmlAttribute + SsmlAttributes ssmlAttributes = new(); + List extraAttributes = null; + + // This is equivalent to a memcpy + ssmlAttributes = ssmAttributesParent; + + string sCulture = null; + CultureInfo culture = null; + while (reader.MoveToNextAttribute()) + { + bool isInvalidAttribute = reader.NamespaceURI != xmlNamespace; + + if (!isInvalidAttribute) + { + switch (reader.LocalName) + { + // The W3C spec says ignore + case "lang": + CheckForDuplicates(ref sCulture, reader); + try + { + culture = new CultureInfo(sCulture); + } + catch (ArgumentException) + { + isInvalidAttribute = true; + } + break; + + default: + isInvalidAttribute = true; + break; + } + } + if (isInvalidAttribute && !ssmlAttributes.AddUnknowAttribute(reader, ref extraAttributes)) + { + ThrowFormatException(SRID.InvalidItemAttribute, reader.Name); + } + } + + // Try to change the voice + bool fNewCulture = culture != null && culture.LCID != ssmlAttributes._fragmentState.LangId; + ssmlAttributes._voice = engine.ProcessTextBlock(isParagraph, ssmlAttributes._voice, ref ssmlAttributes._fragmentState, culture, fNewCulture, ssmlAttributes._gender, ssmlAttributes._age); + if (fNewCulture) + { + ssmlAttributes._fragmentState.LangId = culture.LCID; + } + + // Process child elements. + SsmlElement possibleChild = SsmlElement.AudioMarkTextWithStyle | ElementPromptEngine(ssmlAttributes); + if (isParagraph) + { + possibleChild |= SsmlElement.Sentence; + } + ProcessElement(reader, engine, sElement, possibleChild, ssmlAttributes, fIgnore, extraAttributes); + + engine.EndProcessTextBlock(isParagraph); + + // Notify the engine that the element is processed + engine.EndElement(); + } + + private static void ParsePhoneme(XmlReader reader, ISsmlParser engine, SsmlElement element, SsmlAttributes ssmAttributesParent, bool fIgnore) + { + // Validate the SSML markup + string sElement = ValidateElement(element, SsmlElement.Phoneme, reader.Name); + + // Make a local copy of the ssmlAttribute + SsmlAttributes ssmlAttributes = new(); + List extraAttributes = null; + + // This is equivalent to a memcpy + ssmlAttributes = ssmAttributesParent; + + string sAlphabet = null; + AlphabetType alphabet = AlphabetType.Ipa; + string sPh = null; + char[] aPhoneIds = null; + while (reader.MoveToNextAttribute()) + { + // Namespace must be empty + bool isInvalidAttribute = reader.NamespaceURI.Length != 0; + + if (!isInvalidAttribute) + { + switch (reader.LocalName) + { + case "alphabet": + CheckForDuplicates(ref sAlphabet, reader); + switch (sAlphabet) + { + case "ipa": + alphabet = AlphabetType.Ipa; + break; + + case "sapi": + case "x-sapi": + case "x-microsoft-sapi": + alphabet = AlphabetType.Sapi; + break; + + case "ups": + case "x-ups": + case "x-microsoft-ups": + alphabet = AlphabetType.Ups; + break; + + default: + throw new FormatException(SR.Get(SRID.UnsupportedAlphabet, sAlphabet)); + } + break; + + case "ph": + CheckForDuplicates(ref sPh, reader); + break; + + default: + isInvalidAttribute = true; + break; + } + } + if (isInvalidAttribute && !ssmlAttributes.AddUnknowAttribute(reader, ref extraAttributes)) + { + ThrowFormatException(SRID.InvalidItemAttribute, reader.Name); + } + } + + if (string.IsNullOrEmpty(sPh)) + { + ThrowFormatException(SRID.MissingRequiredAttribute, "ph", "phoneme"); + } + + // Try to convert the phoneme set + try + { + switch (alphabet) + { + case AlphabetType.Sapi: + aPhoneIds = PhonemeConverter.ConvertPronToId(sPh, ssmlAttributes._fragmentState.LangId).ToCharArray(); + break; + + case AlphabetType.Ups: + aPhoneIds = PhonemeConverter.UpsConverter.ConvertPronToId(sPh).ToCharArray(); + alphabet = AlphabetType.Ipa; + break; + + case AlphabetType.Ipa: + default: + aPhoneIds = sPh.ToCharArray(); + try + { + PhonemeConverter.ValidateUpsIds(aPhoneIds); + } + catch (FormatException) + { + if (sAlphabet != null) + { + throw; + } + else + { + // try with sapi (backward compatibility) + // if not a sapi phoneme either throw the IPA exception + aPhoneIds = PhonemeConverter.ConvertPronToId(sPh, ssmlAttributes._fragmentState.LangId).ToCharArray(); + alphabet = AlphabetType.Sapi; + } + } + break; + } + } + catch (FormatException) + { + ThrowFormatException(SRID.InvalidItemAttribute, "phoneme"); + } + + engine.ProcessPhoneme(ref ssmlAttributes._fragmentState, alphabet, sPh, aPhoneIds); + + // Process child elements. + ProcessElement(reader, engine, sElement, SsmlElement.Text, ssmlAttributes, fIgnore, extraAttributes); + + // Notify the engine that the element is processed + engine.EndElement(); + } + + private static void ParseProsody(XmlReader reader, ISsmlParser engine, SsmlElement element, SsmlAttributes ssmAttributesParent, bool fIgnore) + { + // Validate the SSML markup + string sElement = ValidateElement(element, SsmlElement.Prosody, reader.Name); + + // Make a local copy of the ssmlAttribute + SsmlAttributes ssmlAttributes = new(); + List extraAttributes = null; + + // This is equivalent to a memcpy + ssmlAttributes = ssmAttributesParent; + + string sPitch = null; + string sContour = null; + string sRange = null; + string sRate = null; + string sDuration = null; + string sVolume = null; + Prosody prosody = ssmlAttributes._fragmentState.Prosody != null ? ssmlAttributes._fragmentState.Prosody.Clone() : new Prosody(); + while (reader.MoveToNextAttribute()) + { + // Namespace must be empty + bool isInvalidAttribute = reader.NamespaceURI.Length != 0; + + if (!isInvalidAttribute) + { + switch (reader.LocalName) + { + case "pitch": + isInvalidAttribute = ParseNumberHz(reader, ref sPitch, s_pitchNames, s_pitchWords, ref prosody._pitch); + break; + + case "range": + isInvalidAttribute = ParseNumberHz(reader, ref sRange, s_rangeNames, s_rangeWords, ref prosody._range); + break; + + case "rate": + isInvalidAttribute = ParseNumberRelative(reader, ref sRate, s_rateNames, s_rateWords, ref prosody._rate); + break; + + case "volume": + isInvalidAttribute = ParseNumberRelative(reader, ref sVolume, s_volumeNames, s_volumeWords, ref prosody._volume); + break; + + case "duration": + CheckForDuplicates(ref sDuration, reader); + prosody.Duration = ParseCSS2Time(sDuration); + break; + + case "contour": + CheckForDuplicates(ref sContour, reader); + prosody.SetContourPoints(ParseContour(sContour)); + if (prosody.GetContourPoints() == null) { isInvalidAttribute = true; } + break; + + default: + isInvalidAttribute = true; + break; + } + } + if (isInvalidAttribute && !ssmlAttributes.AddUnknowAttribute(reader, ref extraAttributes)) + { + ThrowFormatException(SRID.InvalidItemAttribute, reader.Name); + } + } + + if (string.IsNullOrEmpty(sPitch) && string.IsNullOrEmpty(sContour) && string.IsNullOrEmpty(sRange) && string.IsNullOrEmpty(sRate) && string.IsNullOrEmpty(sDuration) && string.IsNullOrEmpty(sVolume)) + { + ThrowFormatException(SRID.MissingRequiredAttribute, "pitch, contour, range, rate, duration, volume", "prosody"); + } + + ssmlAttributes._fragmentState.Prosody = prosody; + + engine.ProcessProsody(sPitch, sRange, sRate, sVolume, sDuration, sContour); + + // Process child elements. + SsmlElement possibleChild = SsmlElement.ParagraphOrSentence | SsmlElement.AudioMarkTextWithStyle | ElementPromptEngine(ssmlAttributes); + ProcessElement(reader, engine, sElement, possibleChild, ssmlAttributes, fIgnore, extraAttributes); + + // Notify the engine that the element is processed + engine.EndElement(); + } + + private static void ParseSayAs(XmlReader reader, ISsmlParser engine, SsmlElement element, SsmlAttributes ssmAttributesParent, bool fIgnore) + { + // Validate the SSML markup + string sElement = ValidateElement(element, SsmlElement.SayAs, reader.Name); + + // Make a local copy of the ssmlAttribute + SsmlAttributes ssmlAttributes = new(); + List extraAttributes = null; + + // This is equivalent to a memcpy + ssmlAttributes = ssmAttributesParent; + + string sInterpretAs = null; + string sFormat = null; + string sDetail = null; + System.Speech.Synthesis.TtsEngine.SayAs sayAs = new(); + while (reader.MoveToNextAttribute()) + { + // Namespace must be empty + bool isInvalidAttribute = reader.NamespaceURI.Length != 0; + + if (!isInvalidAttribute) + { + switch (reader.LocalName) + { + case "type": + case "interpret-as": + CheckForDuplicates(ref sInterpretAs, reader); + sayAs.InterpretAs = sInterpretAs; + break; + + case "format": + CheckForDuplicates(ref sFormat, reader); + sayAs.Format = sFormat; + break; + + case "detail": + CheckForDuplicates(ref sDetail, reader); + sayAs.Detail = sDetail; + break; + + default: + isInvalidAttribute = true; + break; + } + } + if (isInvalidAttribute && !ssmlAttributes.AddUnknowAttribute(reader, ref extraAttributes)) + { + ThrowFormatException(SRID.InvalidItemAttribute, reader.Name); + } + } + + if (string.IsNullOrEmpty(sInterpretAs)) + { + ThrowFormatException(SRID.MissingRequiredAttribute, "interpret-as", "say-as"); + } + + // Create SayAs attribute + ssmlAttributes._fragmentState.SayAs = sayAs; + + engine.ProcessSayAs(sInterpretAs, sFormat, sDetail); + + // Process child elements. + ProcessElement(reader, engine, sElement, SsmlElement.Text, ssmlAttributes, fIgnore, extraAttributes); + + // Notify the engine that the element is processed + engine.EndElement(); + } + + private static void ParseSub(XmlReader reader, ISsmlParser engine, SsmlElement element, SsmlAttributes ssmAttributesParent, bool fIgnore) + { + // Validate the SSML markup + string sElement = ValidateElement(element, SsmlElement.Sub, reader.Name); + + // Make a local copy of the ssmlAttribute + SsmlAttributes ssmlAttributes = new(); + List extraAttributes = null; + + // This is equivalent to a memcpy + ssmlAttributes = ssmAttributesParent; + + string sAlias = null; + int textPosition = 0; + while (reader.MoveToNextAttribute()) + { + // Namespace must be empty + bool isInvalidAttribute = reader.NamespaceURI.Length != 0; + + if (!isInvalidAttribute) + { + switch (reader.LocalName) + { + // The W3C spec says ignore + case "alias": + CheckForDuplicates(ref sAlias, reader); + XmlTextReader textReader = reader as XmlTextReader; + if (textReader != null && engine.Ssml != null) + { + textPosition = engine.Ssml.IndexOf(reader.Value, textReader.LinePosition + reader.LocalName.Length, StringComparison.Ordinal); + } + break; + + default: + isInvalidAttribute = true; + break; + } + } + if (isInvalidAttribute && !ssmlAttributes.AddUnknowAttribute(reader, ref extraAttributes)) + { + ThrowFormatException(SRID.InvalidItemAttribute, reader.Name); + } + } + + if (string.IsNullOrEmpty(sAlias)) + { + ThrowFormatException(SRID.MissingRequiredAttribute, "alias", "sub"); + } + + engine.ProcessSub(sAlias, ssmlAttributes._voice, ref ssmlAttributes._fragmentState, textPosition, fIgnore); + + // The only allowed children element is text. Ignore it + ProcessElement(reader, engine, sElement, SsmlElement.Text, ssmlAttributes, true, extraAttributes); + + // Notify the engine that the element is processed + engine.EndElement(); + } + private static void ParseVoice(XmlReader reader, ISsmlParser engine, SsmlElement element, SsmlAttributes ssmAttributesParent, bool fIgnore) + { + // Validate the SSML markup + string sElement = ValidateElement(element, SsmlElement.Voice, reader.Name); + + // Cannot have a voice element in a Prompt bout + if (ssmAttributesParent._cPromptOutput > 0) + { + ThrowFormatException(SRID.InvalidVoiceElementInPromptOutput); + } + + // Make a local copy of the ssmlAttribute + SsmlAttributes ssmlAttributes = new(); + + // This is equivalent to a memcpy + ssmlAttributes = ssmAttributesParent; + + string sCulture = null; + string sGender = null; + string sVariant = null; + string sName = null; + string sAge = null; + string xmlns = null; + CultureInfo culture = null; + int variant = -1; + + List extraAttributes = null; + List extraAttributesVoice = null; + List localUnknownNamespaces = null; + + while (reader.MoveToNextAttribute()) + { + bool isInvalidAttribute = false; + + // empty namespace + if (reader.NamespaceURI.Length == 0) + { + switch (reader.LocalName) + { + case "gender": + CheckForDuplicates(ref sGender, reader); + VoiceGender gender; + if (!SsmlParserHelpers.TryConvertGender(sGender, out gender)) + { + isInvalidAttribute = true; + } + else + { + ssmlAttributes._gender = gender; + } + break; + + case "age": + CheckForDuplicates(ref sAge, reader); + VoiceAge age; + if (!SsmlParserHelpers.TryConvertAge(sAge, out age)) + { + isInvalidAttribute = true; + } + else + { + ssmlAttributes._age = age; + } + break; + + case "variant": + // Ignore this field. We have no way with the current tokens to + // use it + CheckForDuplicates(ref sVariant, reader); + if (!int.TryParse(sVariant, out variant) || variant <= 0) + { + isInvalidAttribute = true; + } + break; + + case "name": + CheckForDuplicates(ref sName, reader); + break; + + default: + isInvalidAttribute = true; + break; + } + } + else + { + if (reader.Prefix == "xmlns" && reader.Value == xmlNamespacePrompt) + { + CheckForDuplicates(ref xmlns, reader); + } + else + { + if (reader.NamespaceURI == xmlNamespace) + { + switch (reader.LocalName) + { + // The W3C spec says ignore + case "lang": + CheckForDuplicates(ref sCulture, reader); + try + { + culture = new CultureInfo(sCulture); + } + catch (ArgumentException) + { + isInvalidAttribute = true; + } + break; + + default: + isInvalidAttribute = true; + break; + } + } + else if (reader.NamespaceURI == xmlNamespaceXmlns) + { + if (reader.Value != xmlNamespaceSsml) + { + if (localUnknownNamespaces == null) + { + localUnknownNamespaces = new List(); + } + + SsmlXmlAttribute ns = new(reader.Prefix, reader.LocalName, reader.Value, reader.NamespaceURI); + localUnknownNamespaces.Add(ns); + ssmlAttributes._unknownNamespaces.Add(ns); + } + } + else + { + if (extraAttributesVoice == null) + { + extraAttributesVoice = new List(); + } + extraAttributesVoice.Add(new SsmlXmlAttribute(reader.Prefix, reader.LocalName, reader.Value, reader.NamespaceURI)); + } + } + } + if (isInvalidAttribute && !ssmlAttributes.AddUnknowAttribute(reader, ref extraAttributes)) + { + ThrowFormatException(SRID.InvalidItemAttribute, reader.Name); + } + } + + // append the local attributes to list of unknown attributes + if (extraAttributesVoice != null) + { + foreach (SsmlXmlAttribute attribute in extraAttributesVoice) + { + ssmlAttributes.AddUnknowAttribute(attribute, ref extraAttributes); + } + } + + if (string.IsNullOrEmpty(sCulture) && string.IsNullOrEmpty(sGender) && string.IsNullOrEmpty(sAge) && string.IsNullOrEmpty(sVariant) && string.IsNullOrEmpty(sName) && string.IsNullOrEmpty(xmlns)) + { + ThrowFormatException(SRID.MissingRequiredAttribute, "'xml:lang' or 'gender' or 'age' or 'variant' or 'name'", "voice"); + } + + // Try to change the voice + culture = culture == null ? new CultureInfo(ssmlAttributes._fragmentState.LangId) : culture; + bool fNewCulture = culture.LCID != ssmlAttributes._fragmentState.LangId; + ssmlAttributes._voice = engine.ProcessVoice(sName, culture, ssmlAttributes._gender, ssmlAttributes._age, variant, fNewCulture, localUnknownNamespaces); + ssmlAttributes._fragmentState.LangId = culture.LCID; + + // Process child elements. + SsmlElement possibleChild = SsmlElement.ParagraphOrSentence | SsmlElement.AudioMarkTextWithStyle | ElementPromptEngine(ssmlAttributes); + ProcessElement(reader, engine, sElement, possibleChild, ssmlAttributes, fIgnore, extraAttributes); + + // remove the local namespaces + if (localUnknownNamespaces != null) + { + foreach (SsmlXmlAttribute ns in localUnknownNamespaces) + { + ssmlAttributes._unknownNamespaces.Remove(ns); + } + } + + // Notify the engine that the element is processed + engine.EndElement(); + } + + private static void ParseLexicon(XmlReader reader, ISsmlParser engine, SsmlElement element, SsmlAttributes ssmAttributesParent, bool fIgnore) + { + // Validate the SSML markup + string sElement = ValidateElement(element, SsmlElement.Lexicon, reader.Name); + + // Make a local copy of the ssmlAttribute + SsmlAttributes ssmlAttributes = new(); + List extraAttributes = null; + + // This is equivalent to a memcpy + ssmlAttributes = ssmAttributesParent; + + string sUri = null; + string sMediaType = null; + while (reader.MoveToNextAttribute()) + { + // Namespace must be empty + bool isInvalidAttribute = reader.NamespaceURI.Length != 0; + + if (!isInvalidAttribute) + { + switch (reader.LocalName) + { + case "uri": + CheckForDuplicates(ref sUri, reader); + break; + + case "type": + CheckForDuplicates(ref sMediaType, reader); + break; + + default: + isInvalidAttribute = true; + break; + } + } + if (isInvalidAttribute && !ssmlAttributes.AddUnknowAttribute(reader, ref extraAttributes)) + { + ThrowFormatException(SRID.InvalidItemAttribute, reader.Name); + } + } + + if (string.IsNullOrEmpty(sUri)) + { + ThrowFormatException(SRID.MissingRequiredAttribute, "uri", "lexicon"); + } + + // Add the base path if it exist + Uri uri = new(sUri, UriKind.RelativeOrAbsolute); + if (!uri.IsAbsoluteUri && ssmlAttributes._baseUri != null) + { + sUri = ssmlAttributes._baseUri + '/' + sUri; + uri = new Uri(sUri, UriKind.RelativeOrAbsolute); + } + + engine.ProcessLexicon(uri, sMediaType); + + // No Children allowed. + ProcessElement(reader, engine, sElement, 0, ssmlAttributes, true, extraAttributes); + + // Notify the engine that the element is processed + engine.EndElement(); + } + + #region Prompt Engine + + private delegate bool ProcessPromptEngine0(object voice); + private delegate bool ProcessPromptEngine1(object voice, string value); + + private static void ParsePromptEngine0(XmlReader reader, ISsmlParser engine, SsmlElement elementAllowed, SsmlElement element, ProcessPromptEngine0 process, SsmlAttributes ssmAttributesParent, bool fIgnore) + { + // Validate the SSML markup + string sElement = ValidateElement(elementAllowed, element, reader.Name); + + // Make a local copy of the ssmlAttribute + SsmlAttributes ssmlAttributes = new(); + + // This is equivalent to a memcpy + ssmlAttributes = ssmAttributesParent; + + // No attributes allowed + while (reader.MoveToNextAttribute()) + { + if (reader.NamespaceURI == xmlNamespaceXmlns && reader.Value == xmlNamespacePrompt) + { + engine.ContainsPexml(reader.LocalName); + } + else + { + ThrowFormatException(SRID.InvalidItemAttribute, reader.Name); + } + } + + // Notify the engine that the element is processed + if (!process(ssmlAttributes._voice)) + { + ThrowFormatException(SRID.InvalidElement, reader.Name); + } + + // Process Children + ProcessElement(reader, engine, sElement, SsmlElement.AudioMarkTextWithStyle | ElementPromptEngine(ssmlAttributes), ssmlAttributes, fIgnore, null); + } + + private static string ParsePromptEngine1(XmlReader reader, ISsmlParser engine, SsmlElement elementAllowed, SsmlElement element, string attribute, ProcessPromptEngine1 process, SsmlAttributes ssmAttributesParent, bool fIgnore) + { + // Validate the SSML markup + string sElement = ValidateElement(elementAllowed, element, reader.Name); + + // Make a local copy of the ssmlAttribute + SsmlAttributes ssmlAttributes = new(); + + // This is equivalent to a memcpy + ssmlAttributes = ssmAttributesParent; + + // 1 attribute + string value = null; + while (reader.MoveToNextAttribute()) + { + if (reader.LocalName == attribute) + { + CheckForDuplicates(ref value, reader); + } + else + { + ThrowFormatException(SRID.InvalidItemAttribute, reader.Name); + } + } + + // Notify the engine that the element is processed + if (!process(ssmlAttributes._voice, value)) + { + ThrowFormatException(SRID.InvalidElement, reader.Name); + } + + // No Children allowed + ProcessElement(reader, engine, sElement, SsmlElement.AudioMarkTextWithStyle | ElementPromptEngine(ssmlAttributes), ssmlAttributes, fIgnore, null); + return value; + } + + private static void ParsePromptOutput(XmlReader reader, ISsmlParser engine, SsmlElement element, SsmlAttributes ssmAttributesParent, bool fIgnore) + { + // Increase the ref count for the Prompt output + ssmAttributesParent._cPromptOutput++; + + ParsePromptEngine0(reader, engine, element, SsmlElement.PromptEngineOutput, new ProcessPromptEngine0(engine.BeginPromptEngineOutput), ssmAttributesParent, fIgnore); + + // Notify the engine that the element is processed + engine.EndElement(); + + // Decrease the ref count for the Prompt output + ssmAttributesParent._cPromptOutput--; + engine.EndPromptEngineOutput(ssmAttributesParent._voice); + } + + private static void ParseDiv(XmlReader reader, ISsmlParser engine, SsmlElement element, SsmlAttributes ssmAttributesParent, bool fIgnore) + { + ParsePromptEngine0(reader, engine, element, SsmlElement.PromptEngineDiv, new ProcessPromptEngine0(engine.ProcessPromptEngineDiv), ssmAttributesParent, fIgnore); + + // Notify the engine that the element is processed + engine.EndElement(); + } + + private static void ParseDatabase(XmlReader reader, ISsmlParser engine, SsmlElement element, SsmlAttributes ssmAttributesParent, bool fIgnore) + { + // Validate the SSML markup + string sElement = ValidateElement(element, SsmlElement.PromptEngineDatabase, reader.Name); + + // Make a local copy of the ssmlAttribute + SsmlAttributes ssmlAttributes = new(); + + // This is equivalent to a memcpy + ssmlAttributes = ssmAttributesParent; + + // No attributes allowed + string fname = null; + string delta = null; + string idset = null; + while (reader.MoveToNextAttribute()) + { + // Namespace must be empty + bool isInvalidAttribute = false; + + if (!isInvalidAttribute) + { + switch (reader.LocalName) + { + case "fname": + CheckForDuplicates(ref fname, reader); + break; + + case "idset": + CheckForDuplicates(ref idset, reader); + break; + + case "delta": + CheckForDuplicates(ref delta, reader); + break; + + default: + isInvalidAttribute = true; + break; + } + } + if (isInvalidAttribute) + { + ThrowFormatException(SRID.InvalidItemAttribute, reader.Name); + } + } + // Notify the engine that the element is processed + if (!engine.ProcessPromptEngineDatabase(ssmlAttributes._voice, fname, delta, idset)) + { + ThrowFormatException(SRID.InvalidElement, reader.Name); + } + + // No Children allowed + ProcessElement(reader, engine, sElement, 0, ssmlAttributes, fIgnore, null); + + // Notify the engine that the element is processed + engine.EndElement(); + } + + private static void ParseId(XmlReader reader, ISsmlParser engine, SsmlElement element, SsmlAttributes ssmAttributesParent, bool fIgnore) + { + ParsePromptEngine1(reader, engine, element, SsmlElement.PromptEngineId, "id", new ProcessPromptEngine1(engine.ProcessPromptEngineId), ssmAttributesParent, fIgnore); + + // Notify the engine that the element is processed + engine.EndElement(); + } + + private static void ParseTts(XmlReader reader, ISsmlParser engine, SsmlElement element, SsmlAttributes ssmAttributesParent, bool fIgnore) + { + ParsePromptEngine0(reader, engine, element, SsmlElement.PromptEngineTTS, new ProcessPromptEngine0(engine.BeginPromptEngineTts), ssmAttributesParent, fIgnore); + + // Notify the engine that the element is processed + engine.EndElement(); + engine.EndPromptEngineTts(ssmAttributesParent._voice); + } + + private static void ParseWithTag(XmlReader reader, ISsmlParser engine, SsmlElement element, SsmlAttributes ssmAttributesParent, bool fIgnore) + { + string tag = ParsePromptEngine1(reader, engine, element, SsmlElement.PromptEngineWithTag, "tag", new ProcessPromptEngine1(engine.BeginPromptEngineWithTag), ssmAttributesParent, fIgnore); + + // Notify the engine that the element is processed + engine.EndElement(); + engine.EndPromptEngineWithTag(ssmAttributesParent._voice, tag); + } + + private static void ParseRule(XmlReader reader, ISsmlParser engine, SsmlElement element, SsmlAttributes ssmAttributesParent, bool fIgnore) + { + string name = ParsePromptEngine1(reader, engine, element, SsmlElement.PromptEngineRule, "name", new ProcessPromptEngine1(engine.BeginPromptEngineRule), ssmAttributesParent, fIgnore); + + // Notify the engine that the element is processed + engine.EndElement(); + engine.EndPromptEngineRule(ssmAttributesParent._voice, name); + } + + #endregion + + private static void CheckForDuplicates(ref string dest, XmlReader reader) + { + if (!string.IsNullOrEmpty(dest)) + { + StringBuilder attribute = new(reader.LocalName); + if (reader.NamespaceURI.Length > 0) + { + attribute.Append(reader.NamespaceURI); + attribute.Append(':'); + } + ThrowFormatException(SRID.InvalidAttributeDefinedTwice, reader.Value, attribute); + } + dest = reader.Value; + } + + private static int ParseCSS2Time(string time) + { + time = time.Trim(Helpers._achTrimChars); + int pos = time.IndexOf("ms", StringComparison.Ordinal); + int duration = -1; + float fDuration; + if (pos > 0 && time.Length == pos + 2) + { + if (!float.TryParse(time.Substring(0, pos), out fDuration)) + { + duration = -1; + } + else + { + duration = (int)(fDuration + 0.5); + } + } + else + if ((pos = time.IndexOf('s')) > 0 && time.Length == pos + 1) + { + if (!float.TryParse(time.Substring(0, pos), out fDuration)) + { + duration = -1; + } + else + { + duration = (int)(fDuration * 1000); + } + } + return duration; + } + + private static ContourPoint[] ParseContour(string contour) + { + char[] achContour = contour.ToCharArray(); + List points = new(); + int start = 0; + + try + { + while (start < achContour.Length) + { + bool percent, ignored, hz; + // Form is (0%, +20Hz) + if ((start = NextChar(achContour, start, '(', false, out ignored)) < 0) + { + // End of the string found exit + break; + } + + int comma = NextChar(achContour, start, ',', true, out percent); + int parenthesis = NextChar(achContour, comma, ')', true, out ignored); + + ProsodyNumber timePosition = new(); + ProsodyNumber target = new(); + + // Parse the 2 numbers + if (!percent || !TryParseNumber(contour.Substring(start, comma - (start + 1)), ref timePosition) || timePosition.SsmlAttributeId == ProsodyNumber.AbsoluteNumber) + { + return null; + } + if (!TryParseHz(contour.Substring(comma, parenthesis - (comma + 1)), ref target, true, out hz)) + { + return null; + } + + // First point + if (points.Count == 0) + { + // fake a zero entry if none is provided by duplicating the first entry + if (timePosition.Number > 0 && timePosition.Number < 100) + { + points.Add(new ContourPoint(0, target.Number, ContourPointChangeType.Hz)); + } + } + else + { + // Accept only increasing start points + // Add a 100% if necessary + if (points[points.Count - 1].Start > timePosition.Number) + { + return null; + } + } + + if (timePosition.Number >= 0 && timePosition.Number <= 1) + { + points.Add(new ContourPoint(timePosition.Number, target.Number, (hz ? ContourPointChangeType.Hz : ContourPointChangeType.Percentage))); + } + start = parenthesis; + } + } + catch (InvalidOperationException) + { + return null; + } + + if (points.Count < 1) + { + return null; + } + + // Add a 100% if necessary + if (!points[points.Count - 1].Start.Equals(1.0)) + { + points.Add(new ContourPoint(1, points[points.Count - 1].Change, points[points.Count - 1].ChangeType)); + } + return points.ToArray(); + } + + private static int NextChar(char[] ach, int start, char expected, bool skipDigit, out bool percent) + { + percent = false; + + // skip the whitespace + while (start < ach.Length && (ach[start] == ' ' || ach[start] == '\t' || ach[start] == '\n' || ach[start] == '\r')) + { + start++; + } + + // skip the digits + if (skipDigit) + { + while (start < ach.Length && ach[start] != expected && ((percent = ach[start] == '%') || char.IsDigit(ach[start]) || ach[start] == 'H' || ach[start] == 'z' || ach[start] == '.' || ach[start] == '+' || ach[start] == '-')) + { + start++; + } + + // skip the trailing white spaces + while (start < ach.Length && (ach[start] == ' ' || ach[start] == '\t' || ach[start] == '\n' || ach[start] == '\r')) + { + start++; + } + } + + // Check if we found the character we wanted + if (!(start < ach.Length && ach[start] == expected)) + { + // Check for the end of the string + if (!skipDigit && start == ach.Length) + { + return -1; + } + // bail out + throw new InvalidOperationException(); + } + return start + 1; + } + + private static bool ParseNumberHz(XmlReader reader, ref string attribute, string[] attributeValues, int[] attributeConst, ref ProsodyNumber number) + { + bool isInvalidAttribute = false; + bool isHz; + + CheckForDuplicates(ref attribute, reader); + int pos = Array.BinarySearch(attributeValues, attribute); + if (pos < 0) + { + if (!TryParseHz(attribute, ref number, false, out isHz)) + { + isInvalidAttribute = true; + } + } + else + { + number = new ProsodyNumber(attributeConst[pos]); + } + return isInvalidAttribute; + } + + private static bool ParseNumberRelative(XmlReader reader, ref string attribute, string[] attributeValues, int[] attributeConst, ref ProsodyNumber number) + { + bool isInvalidAttribute = false; + + CheckForDuplicates(ref attribute, reader); + int pos = Array.BinarySearch(attributeValues, attribute); + if (pos < 0) + { + if (!TryParseNumber(attribute, ref number)) + { + isInvalidAttribute = true; + } + } + else + { + number = new ProsodyNumber(attributeConst[pos]); + } + return isInvalidAttribute; + } + + private static bool TryParseNumber(string sNumber, ref ProsodyNumber number) + { + bool fResult = false; + decimal value = 0; + + // always reset the unit to Default + number.Unit = ProsodyUnit.Default; + sNumber = sNumber.Trim(Helpers._achTrimChars); + if (!string.IsNullOrEmpty(sNumber)) + { + if (!decimal.TryParse(sNumber, out value)) + { + if (sNumber[sNumber.Length - 1] == '%') + { + if (decimal.TryParse(sNumber.Substring(0, sNumber.Length - 1), out value)) + { + float percent = (float)value / 100f; + if (sNumber[0] != '+' && sNumber[0] != '-') + { + number.Number = number.Number * percent; + } + else + { + number.Number += number.Number * (percent); + } + + fResult = true; + } + } + } + else + { + if (sNumber[0] != '+' && sNumber[0] != '-') + { + number.Number = (float)value; + number.SsmlAttributeId = ProsodyNumber.AbsoluteNumber; + } + else + { + if (number.IsNumberPercent) + { + number.Number *= (float)value; + } + else + { + number.Number += (float)value; + } + } + number.IsNumberPercent = false; + fResult = true; + } + } + return fResult; + } + + private static bool TryParseHz(string sNumber, ref ProsodyNumber number, bool acceptHzRelative, out bool isHz) + { + isHz = false; + + // Find the Hz at the end of the number + bool fResult = false; + number.SsmlAttributeId = ProsodyNumber.AbsoluteNumber; + ProsodyUnit unit = ProsodyUnit.Default; + + sNumber = sNumber.Trim(Helpers._achTrimChars); + if (sNumber.IndexOf("Hz", StringComparison.Ordinal) == sNumber.Length - 2) + { + unit = ProsodyUnit.Hz; + } + else if (sNumber.IndexOf("st", StringComparison.Ordinal) == sNumber.Length - 2) + { + unit = ProsodyUnit.Semitone; + } + + if (unit != ProsodyUnit.Default) + { + // Try as an Absolute Hz value + fResult = TryParseNumber(sNumber.Substring(0, sNumber.Length - 2), ref number) && (acceptHzRelative || number.SsmlAttributeId == ProsodyNumber.AbsoluteNumber); + isHz = true; + } + else + { + // Must be a relative number + fResult = TryParseNumber(sNumber, ref number) && number.SsmlAttributeId == ProsodyNumber.AbsoluteNumber; + } + + return fResult; + } + + /// + /// Ensure the this element is properly placed in the SSML markup + /// + private static string ValidateElement(SsmlElement possibleElements, SsmlElement currentElement, string sElement) + { + if ((possibleElements & currentElement) == 0) + { + ThrowFormatException(SRID.InvalidElement, sElement); + } + return sElement; + } + + /// + /// Throws an Exception with the error specified by the resource ID. + /// + private static void ThrowFormatException(SRID id, params object[] args) + { + throw new FormatException(SR.Get(id, args)); + } + + /// + /// Throws an Exception with the error specified by the resource ID. + /// + private static void ThrowFormatException(Exception innerException, SRID id, params object[] args) + { + throw new FormatException(SR.Get(id, args), innerException); + } + + /// + /// Non speakable element + /// + private static void NoOp(XmlReader reader, ISsmlParser engine, SsmlElement element, SsmlAttributes ssmlAttributes, bool fIgnore) + { + // No Children allowed . + ProcessElement(reader, engine, null, 0, ssmlAttributes, true, null); + } + + private static SsmlElement ElementPromptEngine(SsmlAttributes ssmlAttributes) + { + return ssmlAttributes._cPromptOutput > 0 ? SsmlElement.PromptEngineChildren : 0; + } + + private static int GetColumnPosition(XmlReader reader) + { + XmlTextReader textReader = reader as XmlTextReader; + return textReader != null ? textReader.LinePosition - 1 : 0; + } + + #endregion + + #region Private Types + + private struct SsmlAttributes + { + internal object _voice; + internal FragmentState _fragmentState; + internal bool _fRenderDesc; + internal VoiceAge _age; + internal VoiceGender _gender; + internal string _baseUri; + internal short _cPromptOutput; + internal List _unknownNamespaces; + + internal bool AddUnknowAttribute(SsmlXmlAttribute attribute, ref List extraAttributes) + { + foreach (SsmlXmlAttribute ns in _unknownNamespaces) + { + if (ns._name == attribute._prefix) + { + if (extraAttributes == null) + { + extraAttributes = new List(); + } + extraAttributes.Add(attribute); + return true; + } + } + return false; + } + + internal bool AddUnknowAttribute(XmlReader reader, ref List extraAttributes) + { + foreach (SsmlXmlAttribute ns in _unknownNamespaces) + { + if (ns._name == reader.Prefix) + { + if (extraAttributes == null) + { + extraAttributes = new List(); + } + extraAttributes.Add(new SsmlXmlAttribute(reader.Prefix, reader.LocalName, reader.Value, reader.NamespaceURI)); + return true; + } + } + return false; + } + + internal bool IsOtherNamespaceElement(XmlReader reader) + { + foreach (SsmlXmlAttribute ns in _unknownNamespaces) + { + if (ns._name == reader.Prefix) + { + return true; + } + } + return false; + } + } + + private delegate void ParseElementDelegates(XmlReader reader, ISsmlParser engine, SsmlElement element, SsmlAttributes ssmlAttributes, bool fIgnore); + + #endregion + + #region Private Fields + + private static readonly string[] s_elementsName = new string[] + { + "audio", + "break", + "database", + "desc", + "div", + "emphasis", + "id", + "lexicon", + "mark", + "meta", + "metadata", + "p", + "paragraph", + "phoneme", + "prompt_output", + "prosody", + "rule", + "s", + "say-as", + "sentence", + "speak", + "sub", + "tts", + "voice", + "withtag", + }; + + private static readonly ParseElementDelegates[] s_parseElements = new ParseElementDelegates[] + { + new ParseElementDelegates (ParseAudio), + new ParseElementDelegates (ParseBreak), + new ParseElementDelegates (ParseDatabase), + new ParseElementDelegates (ParseDesc), + new ParseElementDelegates (ParseDiv), + new ParseElementDelegates (ParseEmphasis), + new ParseElementDelegates (ParseId), + new ParseElementDelegates (ParseLexicon), + new ParseElementDelegates (ParseMark), + new ParseElementDelegates (NoOp), + new ParseElementDelegates (ParseMetaData), + new ParseElementDelegates (ParseParagraph), + new ParseElementDelegates (ParseParagraph), + new ParseElementDelegates (ParsePhoneme), + new ParseElementDelegates (ParsePromptOutput), + new ParseElementDelegates (ParseProsody), + new ParseElementDelegates (ParseRule), + new ParseElementDelegates (ParseSentence), + new ParseElementDelegates (ParseSayAs), + new ParseElementDelegates (ParseSentence), + new ParseElementDelegates (NoOp), + new ParseElementDelegates (ParseSub), + new ParseElementDelegates (ParseTts), + new ParseElementDelegates (ParseVoice), + new ParseElementDelegates (ParseWithTag) + }; + + private static readonly string[] s_breakStrength = new string[] + { + "medium", "none", "strong", "weak", "x-strong", "x-weak" + }; + + /// + /// Must be in the same order as the _breakStrength enumeration + /// + private static readonly EmphasisBreak[] s_breakEmphasis = new EmphasisBreak[] + { + EmphasisBreak.Medium, EmphasisBreak.None, EmphasisBreak.Strong, EmphasisBreak.Weak, EmphasisBreak.ExtraStrong, EmphasisBreak.ExtraWeak + }; + + private static readonly string[] s_emphasisNames = new string[] + { + "moderate", "none", "reduced", "strong" + }; + + /// + /// Must be in the same order as the _emphasisNames enumeration + /// + private static readonly EmphasisWord[] s_emphasisWord = new EmphasisWord[] + { + EmphasisWord.Moderate, EmphasisWord.None, EmphasisWord.Reduced, EmphasisWord.Strong + }; + + /// + /// Must be in the same order as the _emphasisNames enumeration + /// + private static readonly int[] s_pitchWords = new int[] + { + (int) ProsodyPitch.Default, (int) ProsodyPitch.High, (int) ProsodyPitch.Low, (int) ProsodyPitch.Medium, (int) ProsodyPitch.ExtraHigh, (int) ProsodyPitch.ExtraLow + }; + + private static readonly string[] s_pitchNames = new string[] + { + "default", "high", "low", "medium", "x-high", "x-low", + }; + + /// + /// Must be in the same order as the _emphasisNames enumeration + /// + private static readonly int[] s_rangeWords = new int[] + { + (int) ProsodyRange.Default, (int) ProsodyRange.High, (int) ProsodyRange.Low, (int) ProsodyRange.Medium, (int) ProsodyRange.ExtraHigh, (int) ProsodyRange.ExtraLow + }; + + private static readonly string[] s_rangeNames = new string[] + { + "default", "high", "low", "medium", "x-high", "x-low", + }; + + /// + /// Must be in the same order as the _emphasisNames enumeration + /// + private static readonly int[] s_rateWords = new int[] + { + (int) ProsodyRate.Default, (int) ProsodyRate.Fast, (int) ProsodyRate.Medium, (int) ProsodyRate.Slow, (int) ProsodyRate.ExtraFast, (int) ProsodyRate.ExtraSlow + }; + + private static readonly string[] s_rateNames = new string[] + { + "default", "fast", "medium", "slow", "x-fast", "x-slow", + }; + + /// + /// Must be in the same order as the _emphasisNames enumeration + /// + private static readonly int[] s_volumeWords = new int[] + { + (int) ProsodyVolume.Default, (int) ProsodyVolume.Loud, (int) ProsodyVolume.Medium, (int) ProsodyVolume.Silent, (int) ProsodyVolume.Soft, (int) ProsodyVolume.ExtraLoud, (int) ProsodyVolume.ExtraSoft + }; + + private static readonly string[] s_volumeNames = new string[] + { + "default", "loud", "medium", "silent", "soft", "x-loud", "x-soft", + }; + + private const string xmlNamespace = "http://www.w3.org/XML/1998/namespace"; + private const string xmlNamespaceSsml = "http://www.w3.org/2001/10/synthesis"; + private const string xmlNamespaceXmlns = "http://www.w3.org/2000/xmlns/"; + private const string xmlNamespacePrompt = "http://schemas.microsoft.com/Speech/2003/03/PromptEngine"; + + #endregion + } + + internal static class SsmlParserHelpers + { + internal static bool TryConvertAge(string sAge, out VoiceAge age) + { + bool fResult = false; + int iAge; + age = VoiceAge.NotSet; + + switch (sAge) + { + case "child": + age = VoiceAge.Child; + break; + + case "teenager": + case "teen": + age = VoiceAge.Teen; + break; + + case "adult": + age = VoiceAge.Adult; + break; + + case "elder": + case "senior": + age = VoiceAge.Senior; + break; + } + if (age != VoiceAge.NotSet) + { + fResult = true; + } + else if (int.TryParse(sAge, out iAge)) + { + if (iAge <= ((int)VoiceAge.Teen + (int)VoiceAge.Child) / 2) + { + age = VoiceAge.Child; + } + else if (iAge <= ((int)VoiceAge.Adult + (int)VoiceAge.Teen) / 2) + { + age = VoiceAge.Teen; + } + else if (iAge <= ((int)VoiceAge.Senior + (int)VoiceAge.Adult) / 2) + { + age = VoiceAge.Adult; + } + else + { + age = VoiceAge.Senior; + } + fResult = true; + } + return fResult; + } + + internal static bool TryConvertGender(string sGender, out VoiceGender gender) + { + bool fResult = false; + gender = VoiceGender.NotSet; + + int pos = Array.BinarySearch(s_genderNames, sGender); + if (pos >= 0) + { + gender = s_genders[pos]; + fResult = true; + } + return fResult; + } + + private static readonly string[] s_genderNames = new string[] + { + "female", "male", "neutral" + }; + + /// + /// Must be in the same order as the _genderNames enumeration + /// + private static readonly VoiceGender[] s_genders = new VoiceGender[] + { + VoiceGender.Female, VoiceGender.Male, VoiceGender.Neutral + }; + } + + #region Internal Types + + [Flags] + internal enum SsmlElement + { + Speak = 0x0001, + Voice = 0x0002, + Audio = 0x0004, + Lexicon = 0x0008, + Meta = 0x0010, + MetaData = 0x0020, + Sentence = 0x0040, + Paragraph = 0x0080, + SayAs = 0x0100, + Phoneme = 0x0200, + Sub = 0x0400, + Emphasis = 0x0800, + Break = 0x1000, + Prosody = 0x2000, + Mark = 0x4000, + Desc = 0x8000, + Text = 0x10000, + PromptEngineOutput = 0x20000, + PromptEngineDatabase = 0x40000, + PromptEngineDiv = 0x80000, + PromptEngineId = 0x100000, + PromptEngineTTS = 0x200000, + PromptEngineWithTag = 0x400000, + PromptEngineRule = 0x800000, + + ParagraphOrSentence = Sentence | Paragraph, + + AudioMarkTextWithStyle = Audio | Mark | Break | Emphasis | Phoneme | Prosody | SayAs | Sub | Voice | Text | PromptEngineOutput, + PromptEngineChildren = PromptEngineDatabase | PromptEngineDiv | PromptEngineId | PromptEngineTTS | PromptEngineWithTag | PromptEngineRule + } + + #endregion +} diff --git a/src/libraries/System.Speech/src/Internal/Synthesis/SafeNativeMethods.cs b/src/libraries/System.Speech/src/Internal/Synthesis/SafeNativeMethods.cs new file mode 100644 index 00000000000000..f0c380b5e919e5 --- /dev/null +++ b/src/libraries/System.Speech/src/Internal/Synthesis/SafeNativeMethods.cs @@ -0,0 +1,216 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Runtime.InteropServices; + +namespace System.Speech.Internal.Synthesis +{ + // This class *MUST* be internal for security purposes + //CASRemoval:[SuppressUnmanagedCodeSecurity] + internal static class SafeNativeMethods + { + /// + /// This function prepares a waveform data block for playback. + /// + /// Handle to the waveform-audio output device. + /// Pointer to a WaveHeader structure that identifies the data + /// block to be prepared. The buffer's base address must be aligned with the + /// respect to the sample size. + /// Size, in bytes, of the WaveHeader structure. + /// MMSYSERR + [DllImport("winmm.dll")] + internal static extern MMSYSERR waveOutPrepareHeader(IntPtr hwo, IntPtr pwh, int cbwh); + + /// + /// This function sends a data block to the specified waveform output device. + /// + /// Handle to the waveform-audio output device. + /// Pointer to a WaveHeader structure containing information + /// about the data block. + /// Size, in bytes, of the WaveHeader structure. + /// MMSYSERR + [DllImport("winmm.dll")] + internal static extern MMSYSERR waveOutWrite(IntPtr hwo, IntPtr pwh, int cbwh); + + /// + /// This function cleans up the preparation performed by waveOutPrepareHeader. + /// The function must be called after the device driver is finished with a data + /// block. You must call this function before freeing the data buffer. + /// + /// Handle to the waveform-audio output device. + /// Pointer to a WaveHeader structure identifying the data block + /// to be cleaned up. + /// Size, in bytes, of the WaveHeader structure. + /// MMSYSERR + [DllImport("winmm.dll")] + internal static extern MMSYSERR waveOutUnprepareHeader(IntPtr hwo, IntPtr pwh, int cbwh); + + /// + /// This function opens a specified waveform output device for playback. + /// + /// Address filled with a handle identifying the open + /// waveform-audio output device. Use the handle to identify the device + /// when calling other waveform-audio output functions. This parameter might + /// be NULL if the WAVE_FORMAT_QUERY flag is specified for fdwOpen. + /// Identifier of the waveform-audio output device to + /// open. It can be either a device identifier or a Handle to an open + /// waveform-audio input device. + /// Pointer to a WaveFormat structure that identifies + /// the format of the waveform-audio data to be sent to the device. You can + /// free this structure immediately after passing it to waveOutOpen. + /// Specifies the address of a fixed callback function, + /// an event handle, a handle to a window, or the identifier of a thread to be + /// called during waveform-audio playback to process messages related to the + /// progress of the playback. If no callback function is required, this value + /// can be zero. + /// Specifies user-instance data passed to the + /// callback mechanism. This parameter is not used with the window callback + /// mechanism. + /// Flags for opening the device. + /// MMSYSERR + [DllImport("winmm.dll")] + internal static extern MMSYSERR waveOutOpen(ref IntPtr phwo, int uDeviceID, byte[] pwfx, WaveOutProc dwCallback, IntPtr dwInstance, uint fdwOpen); + + /// + /// This function closes the specified waveform output device. + /// + /// Handle to the waveform-audio output device. If the function + /// succeeds, the handle is no longer valid after this call. + /// MMSYSERR + [DllImport("winmm.dll")] + internal static extern MMSYSERR waveOutClose(IntPtr hwo); + + /// + /// This function stops playback on a specified waveform output device and + /// resets the current position to 0. All pending playback buffers are marked + /// as done and returned to the application. + /// + /// Handle to the waveform-audio output device. + /// MMSYSERR + [DllImport("winmm.dll")] + internal static extern MMSYSERR waveOutReset(IntPtr hwo); + + /// + /// This function pauses playback on a specified waveform output device. The + /// current playback position is saved. Use waveOutRestart to resume playback + /// from the current playback position. + /// + /// Handle to the waveform-audio output device. + /// MMSYSERR + [DllImport("winmm.dll")] + internal static extern MMSYSERR waveOutPause(IntPtr hwo); + + /// + /// This function restarts a paused waveform output device. + /// + /// Handle to the waveform-audio output device. + /// MMSYSERR + [DllImport("winmm.dll")] + internal static extern MMSYSERR waveOutRestart(IntPtr hwo); + + internal delegate void WaveOutProc(IntPtr hwo, MM_MSG uMsg, IntPtr dwInstance, IntPtr dwParam1, IntPtr dwParam2); + +#pragma warning disable CA1823 // unused fields + internal struct WAVEOUTCAPS + { + private ushort wMid; + private ushort wPid; + private uint vDriverVersion; + [MarshalAs(UnmanagedType.ByValTStr, SizeConst = 32)] + internal string szPname; + private uint dwFormats; + private ushort wChannels; + private ushort wReserved1; + private ushort dwSupport; + } +#pragma warning restore CA1823 + + /// + /// This function queries a specified waveform device to determine its + /// capabilities. + /// + /// Identifier of the waveform-audio output device. + /// It can be either a device identifier or a Handle to an open waveform-audio + /// output device. + /// Pointer to a WAVEOUTCAPS structure to be filled with + /// information about the capabilities of the device. + /// Size, in bytes, of the WAVEOUTCAPS structure. + /// MMSYSERR + [DllImport("winmm.dll")] + internal static extern MMSYSERR waveOutGetDevCaps(IntPtr uDeviceID, ref WAVEOUTCAPS caps, int cbwoc); + + /// + /// This function retrieves the number of waveform output devices present + /// in the system. + /// + /// The number of devices indicates success. Zero indicates that + /// no devices are present or that an error occurred. + [DllImport("winmm.dll")] + internal static extern int waveOutGetNumDevs(); + + // Used by MMTIME.wType + internal const uint TIME_MS = 0x0001; + internal const uint TIME_SAMPLES = 0x0002; + internal const uint TIME_BYTES = 0x0004; + internal const uint TIME_TICKS = 0x0020; + + // Flag specifying the use of a callback window for sound messages + internal const uint CALLBACK_WINDOW = 0x10000; + internal const uint CALLBACK_NULL = 0x00000000; + internal const uint CALLBACK_FUNCTION = 0x00030000; + } + + #region Internal Types + + /// + /// MM WAVEHDR structure + /// + [StructLayout(LayoutKind.Sequential)] + internal struct WAVEHDR + { + internal IntPtr lpData; // disposed by the GCHandle + internal uint dwBufferLength; + internal uint dwBytesRecorded; + internal uint dwUser; + internal uint dwFlags; + internal uint dwLoops; + internal IntPtr lpNext; // unused + internal uint reserved; + } + + // Enum equivalent to MMSYSERR_* + internal enum MMSYSERR : int + { + NOERROR = 0, + ERROR = (1), + BADDEVICEID = (2), + NOTENABLED = (3), + ALLOCATED = (4), + INVALHANDLE = (5), + NODRIVER = (6), + NOMEM = (7), + NOTSUPPORTED = (8), + BADERRNUM = (9), + INVALFLAG = (10), + INVALPARAM = (11), + HANDLEBUSY = (12), + INVALIDALIAS = (13), + BADDB = (14), + KEYNOTFOUND = (15), + READERROR = (16), + WRITEERROR = (17), + DELETEERROR = (18), + VALNOTFOUND = (19), + NODRIVERCB = (20), + LASTERROR = (20) + } + + internal enum MM_MSG + { + MM_WOM_OPEN = 0x03BB, + MM_WOM_CLOSE = 0x03BC, + MM_WOM_DONE = 0x03BD + } + + #endregion +} diff --git a/src/libraries/System.Speech/src/Internal/Synthesis/SpeakInfo.cs b/src/libraries/System.Speech/src/Internal/Synthesis/SpeakInfo.cs new file mode 100644 index 00000000000000..ee73b6e1f4a660 --- /dev/null +++ b/src/libraries/System.Speech/src/Internal/Synthesis/SpeakInfo.cs @@ -0,0 +1,167 @@ +// 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.Globalization; +using System.IO; +using System.Speech.Synthesis; +using System.Speech.Synthesis.TtsEngine; + +#pragma warning disable 56524 // The _voiceSynthesis member is not created in this module and should not be disposed + +namespace System.Speech.Internal.Synthesis +{ + internal sealed class SpeakInfo + { + #region Constructors + /// Voice synthesizer used + /// Default engine to use + internal SpeakInfo(VoiceSynthesis voiceSynthesis, TTSVoice ttsVoice) + { + _voiceSynthesis = voiceSynthesis; + _ttsVoice = ttsVoice; + } + + #endregion + + #region Internal Properties + + internal TTSVoice Voice + { + get + { + return _ttsVoice; + } + } + + #endregion + + #region Internal Methods + + internal void SetVoice(string name, CultureInfo culture, VoiceGender gender, VoiceAge age, int variant) + { + TTSVoice ttsVoice = _voiceSynthesis.GetEngine(name, culture, gender, age, variant, false); + if (!ttsVoice.Equals(_ttsVoice)) + { + _ttsVoice = ttsVoice; + _fNotInTextSeg = true; + } + } + + internal void AddAudio(AudioData audio) + { + AddNewSeg(null, audio); + _fNotInTextSeg = true; + } + + internal void AddText(TTSVoice ttsVoice, TextFragment textFragment) + { + if (_fNotInTextSeg || ttsVoice != _ttsVoice) + { + AddNewSeg(ttsVoice, null); + _fNotInTextSeg = false; + } + _lastSeg.AddFrag(textFragment); + } + + internal SpeechSeg RemoveFirst() + { + SpeechSeg speechSeg = null; + if (_listSeg.Count > 0) + { + speechSeg = _listSeg[0]; + _listSeg.RemoveAt(0); + } + return speechSeg; + } + + #endregion + + #region Private Method + + private void AddNewSeg(TTSVoice pCurrVoice, AudioData audio) + { + SpeechSeg pNew = new(pCurrVoice, audio); + + _listSeg.Add(pNew); + _lastSeg = pNew; + } + + #endregion + + #region private Fields + + // default TTS voice + private TTSVoice _ttsVoice; + + // If true then a new segment is required for the next Add Text + private bool _fNotInTextSeg = true; + + // list of segments (text or audio) + private List _listSeg = new(); + + // current segment + private SpeechSeg _lastSeg; + + // Reference to the VoiceSynthesizer that created it + private VoiceSynthesis _voiceSynthesis; + + #endregion + } + + #region Private Types + + internal class AudioData : IDisposable + { + internal AudioData(Uri uri, ResourceLoader resourceLoader) + { + _uri = uri; + _resourceLoader = resourceLoader; + Uri baseAudio; + _stream = _resourceLoader.LoadFile(uri, out _mimeType, out baseAudio, out _localFile); + } + + /// + /// Needed by IEnumerable!!! + /// + public void Dispose() + { + Dispose(true); + GC.SuppressFinalize(this); + } + + ~AudioData() + { + Dispose(false); + } + + internal Uri _uri; + internal string _mimeType; + internal Stream _stream; + + protected virtual void Dispose(bool disposing) + { + if (disposing) + { + // unload the file from the cache + if (_localFile != null) + { + _resourceLoader.UnloadFile(_localFile); + } + + if (_stream != null) + { + _stream.Dispose(); + _stream = null; + _localFile = null; + _uri = null; + } + } + } + + private string _localFile; + private ResourceLoader _resourceLoader; + } + + #endregion +} diff --git a/src/libraries/System.Speech/src/Internal/Synthesis/SpeechSeg.cs b/src/libraries/System.Speech/src/Internal/Synthesis/SpeechSeg.cs new file mode 100644 index 00000000000000..2fe2edb936b298 --- /dev/null +++ b/src/libraries/System.Speech/src/Internal/Synthesis/SpeechSeg.cs @@ -0,0 +1,83 @@ +// 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.Speech.Synthesis.TtsEngine; + +namespace System.Speech.Internal.Synthesis +{ + + internal class SpeechSeg + { + #region Constructors + + internal SpeechSeg(TTSVoice voice, AudioData audio) + { + _voice = voice; + _audio = audio; + } + + #endregion + + #region Internal Properties + + internal List FragmentList + { + get + { + return _textFragments; + } + } + + internal AudioData Audio + { + get + { + return _audio; + } + } + + internal TTSVoice Voice + { + get + { + return _voice; + } + } + + internal bool IsText + { + get + { + return _audio == null; + } + } + + #endregion + + #region Internal Methods + + internal void AddFrag(TextFragment textFragment) + { + if (_audio != null) + { + throw new InvalidOperationException(); + } + + _textFragments.Add(textFragment); + } + + #endregion + + #region private Fields + + private TTSVoice _voice; + private List _textFragments = new(); +#pragma warning disable 56524 // The _audio are not created in this module and should not be disposed + private AudioData _audio; +#pragma warning restore 56524 + + #endregion + + } +} diff --git a/src/libraries/System.Speech/src/Internal/Synthesis/TTSEngineProxy.cs b/src/libraries/System.Speech/src/Internal/Synthesis/TTSEngineProxy.cs new file mode 100644 index 00000000000000..b405ecda756b15 --- /dev/null +++ b/src/libraries/System.Speech/src/Internal/Synthesis/TTSEngineProxy.cs @@ -0,0 +1,212 @@ +// 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.Runtime.InteropServices; +using System.Speech.Synthesis.TtsEngine; + +namespace System.Speech.Internal.Synthesis +{ + internal abstract class ITtsEngineProxy + { + internal ITtsEngineProxy(int lcid) + { + _alphabetConverter = new AlphabetConverter(lcid); + } + + internal abstract IntPtr GetOutputFormat(IntPtr targetFormat); + internal abstract void AddLexicon(Uri lexicon, string mediaType); + internal abstract void RemoveLexicon(Uri lexicon); + internal abstract void Speak(List frags, byte[] wfx); + internal abstract void ReleaseInterface(); + internal abstract char[] ConvertPhonemes(char[] phones, AlphabetType alphabet); + internal abstract AlphabetType EngineAlphabet { get; } + internal AlphabetConverter AlphabetConverter { get { return _alphabetConverter; } } + + protected AlphabetConverter _alphabetConverter; + } + + internal class TtsProxySsml : ITtsEngineProxy + { + #region Constructors + + internal TtsProxySsml(TtsEngineSsml ssmlEngine, ITtsEngineSite site, int lcid) + : base(lcid) + { + _ssmlEngine = ssmlEngine; + _site = site; + } + + #endregion + + #region Internal Methods + + internal override IntPtr GetOutputFormat(IntPtr targetFormat) + { + return _ssmlEngine.GetOutputFormat(SpeakOutputFormat.WaveFormat, targetFormat); + } + + internal override void AddLexicon(Uri lexicon, string mediaType) + { + _ssmlEngine.AddLexicon(lexicon, mediaType, _site); + } + + internal override void RemoveLexicon(Uri lexicon) + { + _ssmlEngine.RemoveLexicon(lexicon, _site); + } + + internal override void Speak(List frags, byte[] wfx) + { + GCHandle gc = GCHandle.Alloc(wfx, GCHandleType.Pinned); + try + { + IntPtr waveFormat = gc.AddrOfPinnedObject(); + _ssmlEngine.Speak(frags.ToArray(), waveFormat, _site); + } + finally + { + gc.Free(); + } + } + + internal override char[] ConvertPhonemes(char[] phones, AlphabetType alphabet) + { + if (alphabet == AlphabetType.Ipa) + { + return phones; + } + else + { + return _alphabetConverter.SapiToIpa(phones); + } + } + + internal override AlphabetType EngineAlphabet + { + get + { + return AlphabetType.Ipa; + } + } + + /// + /// Release the COM interface for COM object + /// + internal override void ReleaseInterface() + { + } + + #endregion + + #region private Fields + + private TtsEngineSsml _ssmlEngine; + private ITtsEngineSite _site; + + #endregion + } + + internal class TtsProxySapi : ITtsEngineProxy + { + #region Constructors + + internal TtsProxySapi(ITtsEngine sapiEngine, IntPtr iSite, int lcid) + : base(lcid) + { + _iSite = iSite; + _sapiEngine = sapiEngine; + } + + #endregion + + #region Internal Methods + + internal override IntPtr GetOutputFormat(IntPtr preferedFormat) + { + // Initialize TTS Engine + Guid formatId = SAPIGuids.SPDFID_WaveFormatEx; + Guid guidNull = new(); + IntPtr coMem = IntPtr.Zero; + + _sapiEngine.GetOutputFormat(ref formatId, preferedFormat, out guidNull, out coMem); + return coMem; + } + + internal override void AddLexicon(Uri lexicon, string mediaType) + { + // SAPI: Ignore + } + + internal override void RemoveLexicon(Uri lexicon) + { + // SAPI: Ignore + } + + internal override void Speak(List frags, byte[] wfx) + { + GCHandle gc = GCHandle.Alloc(wfx, GCHandleType.Pinned); + try + { + IntPtr waveFormat = gc.AddrOfPinnedObject(); + GCHandle spvTextFragment = new(); + + if (ConvertTextFrag.ToSapi(frags, ref spvTextFragment)) + { + Guid formatId = SAPIGuids.SPDFID_WaveFormatEx; + try + { + _sapiEngine.Speak(0, ref formatId, waveFormat, spvTextFragment.AddrOfPinnedObject(), _iSite); + } + finally + { + ConvertTextFrag.FreeTextSegment(ref spvTextFragment); + } + } + } + finally + { + gc.Free(); + } + } + + internal override AlphabetType EngineAlphabet + { + get + { + return AlphabetType.Sapi; + } + } + + internal override char[] ConvertPhonemes(char[] phones, AlphabetType alphabet) + { + if (alphabet == AlphabetType.Ipa) + { + return _alphabetConverter.IpaToSapi(phones); + } + else + { + return phones; + } + } + + /// + /// Release the COM interface for COM object + /// + internal override void ReleaseInterface() + { + Marshal.ReleaseComObject(_sapiEngine); + } + + #endregion + + #region private Fields + + private ITtsEngine _sapiEngine; + + // This variable is stored here but never created or deleted + private IntPtr _iSite; + + #endregion + } +} diff --git a/src/libraries/System.Speech/src/Internal/Synthesis/TTSEvent.cs b/src/libraries/System.Speech/src/Internal/Synthesis/TTSEvent.cs new file mode 100644 index 00000000000000..d2f578e8ddc1f5 --- /dev/null +++ b/src/libraries/System.Speech/src/Internal/Synthesis/TTSEvent.cs @@ -0,0 +1,178 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Speech.Synthesis; +using System.Speech.Synthesis.TtsEngine; + +namespace System.Speech.Internal.Synthesis +{ + + internal class TTSEvent + { + #region Constructors + + internal TTSEvent(TtsEventId id, Prompt prompt, Exception exception, VoiceInfo voice) + { + _evtId = id; + _prompt = prompt; + _exception = exception; + _voice = voice; + } + + internal TTSEvent(TtsEventId id, Prompt prompt, Exception exception, VoiceInfo voice, TimeSpan audioPosition, long streamPosition, string bookmark, uint wParam, IntPtr lParam) + : this(id, prompt, exception, voice) + { + _audioPosition = audioPosition; + _bookmark = bookmark; + _wParam = wParam; + _lParam = lParam; + } + + private TTSEvent() + { + } + + internal static TTSEvent CreatePhonemeEvent(string phoneme, string nextPhoneme, + TimeSpan duration, SynthesizerEmphasis emphasis, + Prompt prompt, TimeSpan audioPosition) + { + TTSEvent ttsEvent = new(); + ttsEvent._evtId = TtsEventId.Phoneme; + ttsEvent._audioPosition = audioPosition; + ttsEvent._prompt = prompt; + ttsEvent._phoneme = phoneme; + ttsEvent._nextPhoneme = nextPhoneme; + ttsEvent._phonemeDuration = duration; + ttsEvent._phonemeEmphasis = emphasis; + + return ttsEvent; + } + + #endregion + + #region Internal Properties + + internal TtsEventId Id + { + get + { + return _evtId; + } + } + + internal Exception Exception + { + get + { + return _exception; + } + } + + internal Prompt Prompt + { + get + { + return _prompt; + } + } + + internal VoiceInfo Voice + { + get + { + return _voice; + } + } + + internal TimeSpan AudioPosition + { + get + { + return _audioPosition; + } + } + + internal string Bookmark + { + get + { + return _bookmark; + } + } + + internal IntPtr LParam + { + get + { + return _lParam; + } + } + + internal uint WParam + { + get + { + return _wParam; + } + } + + internal SynthesizerEmphasis PhonemeEmphasis + { + get + { + return _phonemeEmphasis; + } + } + + internal string Phoneme + { + get + { + return _phoneme; + } + } + + internal string NextPhoneme + { + get + { + return _nextPhoneme; + } + set + { + _nextPhoneme = value; + } + } + + internal TimeSpan PhonemeDuration + { + get + { + return _phonemeDuration; + } + } + + #endregion + + #region private Fields + + private TtsEventId _evtId; + private Exception _exception; + private VoiceInfo _voice; + private TimeSpan _audioPosition; + private string _bookmark; + private uint _wParam; + private IntPtr _lParam; + private Prompt _prompt; + + // + // Data for phoneme event + // + private string _phoneme; + private string _nextPhoneme; + private TimeSpan _phonemeDuration; + private SynthesizerEmphasis _phonemeEmphasis; + #endregion + + } +} diff --git a/src/libraries/System.Speech/src/Internal/Synthesis/TTSVoice.cs b/src/libraries/System.Speech/src/Internal/Synthesis/TTSVoice.cs new file mode 100644 index 00000000000000..6bdec2eeedd53f --- /dev/null +++ b/src/libraries/System.Speech/src/Internal/Synthesis/TTSVoice.cs @@ -0,0 +1,158 @@ +// 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.Runtime.InteropServices; +using System.Speech.Synthesis; + +namespace System.Speech.Internal.Synthesis +{ + internal class TTSVoice + { + #region Constructors + + internal TTSVoice(ITtsEngineProxy engine, VoiceInfo voiceId) + { + _engine = engine; + _voiceId = voiceId; + } + + #endregion + + #region public Methods + + /// + /// Tests whether two objects are equivalent + /// + public override bool Equals(object obj) + { + TTSVoice voice = obj as TTSVoice; + return voice != null && (_voiceId.Equals(voice.VoiceInfo)); + } + + /// + /// Overrides Object.GetHashCode() + /// + public override int GetHashCode() + { + return _voiceId.GetHashCode(); + } + + #endregion + + #region Internal Methods + + internal void UpdateLexicons(List lexicons) + { + // Remove the lexicons that are defined in this voice but are not in the list + for (int i = _lexicons.Count - 1; i >= 0; i--) + { + LexiconEntry entry = _lexicons[i]; + if (!lexicons.Contains(entry)) + { + // Remove the entry first, just in case the RemoveLexicon throws + _lexicons.RemoveAt(i); + TtsEngine.RemoveLexicon(entry._uri); + } + } + + // Add the lexicons that are defined in this voice but are not in the list + foreach (LexiconEntry entry in lexicons) + { + if (!_lexicons.Contains(entry)) + { + // Remove the entry first, just in case the RemoveLexicon throws + TtsEngine.AddLexicon(entry._uri, entry._mediaType); + _lexicons.Add(entry); + } + } + } + + internal byte[] WaveFormat(byte[] targetWaveFormat) + { + // Get the Wave header if it has not been set by the user + if (targetWaveFormat == null && _waveFormat == null) + { + // The registry values contains a default rate + if (VoiceInfo.SupportedAudioFormats.Count > 0) + { + // Create the array of bytes containing the format + targetWaveFormat = VoiceInfo.SupportedAudioFormats[0].WaveFormat; + } + } + + // No input specified and we already got the default + if (targetWaveFormat == null && _waveFormat != null) + { + return _waveFormat; + } + + // New waveFormat provided? + if (_waveFormat == null || !Array.Equals(targetWaveFormat, _waveFormat)) + { + IntPtr waveFormat = IntPtr.Zero; + GCHandle targetFormat = new(); + + if (targetWaveFormat != null) + { + targetFormat = GCHandle.Alloc(targetWaveFormat, GCHandleType.Pinned); + } + try + { + waveFormat = _engine.GetOutputFormat(targetWaveFormat != null ? targetFormat.AddrOfPinnedObject() : IntPtr.Zero); + } + finally + { + if (targetWaveFormat != null) + { + targetFormat.Free(); + } + } + + if (waveFormat != IntPtr.Zero) + { + _waveFormat = WAVEFORMATEX.ToBytes(waveFormat); + + // Free the buffer + Marshal.FreeCoTaskMem(waveFormat); + } + else + { + _waveFormat = WAVEFORMATEX.Default.ToBytes(); + } + } + return _waveFormat; + } + + #endregion + + #region Internal Properties + + internal ITtsEngineProxy TtsEngine + { + get + { + return _engine; + } + } + + internal VoiceInfo VoiceInfo + { + get + { + return _voiceId; + } + } + + #endregion + + #region private Fields + + private ITtsEngineProxy _engine; + private VoiceInfo _voiceId; + private List _lexicons = new(); + private byte[] _waveFormat; + + #endregion + } +} diff --git a/src/libraries/System.Speech/src/Internal/Synthesis/TextFragmentEngine.cs b/src/libraries/System.Speech/src/Internal/Synthesis/TextFragmentEngine.cs new file mode 100644 index 00000000000000..79c64e3799cd25 --- /dev/null +++ b/src/libraries/System.Speech/src/Internal/Synthesis/TextFragmentEngine.cs @@ -0,0 +1,321 @@ +// 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.Globalization; +using System.IO; +using System.Speech.Synthesis; +using System.Speech.Synthesis.TtsEngine; +using System.Text; +using System.Xml; + +namespace System.Speech.Internal.Synthesis +{ + internal class TextFragmentEngine : ISsmlParser + { + #region Constructors + + internal TextFragmentEngine(SpeakInfo speakInfo, string ssmlText, bool pexml, ResourceLoader resourceLoader, List lexicons) + { + _lexicons = lexicons; + _ssmlText = ssmlText; + _speakInfo = speakInfo; + _resourceLoader = resourceLoader; + } + + #endregion + + #region Internal Methods + + public object ProcessSpeak(string sVersion, string sBaseUri, CultureInfo culture, List extraNamespace) + { + _speakInfo.SetVoice(null, culture, VoiceGender.NotSet, VoiceAge.NotSet, 1); + return _speakInfo.Voice; + } + + public void ProcessText(string text, object voice, ref FragmentState fragmentState, int position, bool fIgnore) + { + if (!fIgnore) + { + TtsEngineAction action = fragmentState.Action; + if (_paragraphStarted) + { + fragmentState.Action = TtsEngineAction.StartParagraph; + _speakInfo.AddText((TTSVoice)voice, new TextFragment(fragmentState)); + _paragraphStarted = false; + + // Always add the start sentence. + _sentenceStarted = true; + } + if (_sentenceStarted) + { + fragmentState.Action = TtsEngineAction.StartSentence; + _speakInfo.AddText((TTSVoice)voice, new TextFragment(fragmentState)); + _sentenceStarted = false; + } + fragmentState.Action = ActionTextFragment(action); + _speakInfo.AddText((TTSVoice)voice, new TextFragment(fragmentState, text, _ssmlText, position, text.Length)); + fragmentState.Action = action; + } + } + + public void ProcessAudio(object voice, string sUri, string baseUri, bool fIgnore) + { + if (!fIgnore) + { + // Prepend the base Uri if necessary + Uri uri = new(sUri, UriKind.RelativeOrAbsolute); + if (!uri.IsAbsoluteUri && !string.IsNullOrEmpty(baseUri)) + { + if (baseUri[baseUri.Length - 1] != '/' && baseUri[baseUri.Length - 1] != '\\') + { + int posSlash = baseUri.LastIndexOf('/'); + if (posSlash < 0) + { + posSlash = baseUri.LastIndexOf('\\'); + } + if (posSlash >= 0) + { + baseUri = baseUri.Substring(0, posSlash); + } + baseUri += '/'; + } + StringBuilder sb = new(baseUri); + sb.Append(sUri); + uri = new Uri(sb.ToString(), UriKind.RelativeOrAbsolute); + } + + // This checks if we can read the file + { + _speakInfo.AddAudio(new AudioData(uri, _resourceLoader)); + } + } + } + + public void ProcessBreak(object voice, ref FragmentState fragmentState, EmphasisBreak eBreak, int time, bool fIgnore) + { + if (!fIgnore) + { + TtsEngineAction action = fragmentState.Action; + fragmentState.Action = ActionTextFragment(fragmentState.Action); + _speakInfo.AddText((TTSVoice)voice, new TextFragment(fragmentState)); + fragmentState.Action = action; + } + } + + public void ProcessDesc(CultureInfo culture) + { + } + + public void ProcessEmphasis(bool noLevel, EmphasisWord word) + { + } + + public void ProcessMark(object voice, ref FragmentState fragmentState, string name, bool fIgnore) + { + if (!fIgnore) + { + TtsEngineAction action = fragmentState.Action; + fragmentState.Action = ActionTextFragment(fragmentState.Action); + _speakInfo.AddText((TTSVoice)voice, new TextFragment(fragmentState, name)); + fragmentState.Action = action; + } + } + + public object ProcessTextBlock(bool isParagraph, object voice, ref FragmentState fragmentState, CultureInfo culture, bool newCulture, VoiceGender gender, VoiceAge age) + { + if (culture != null && newCulture) + { + _speakInfo.SetVoice(null, culture, gender, age, 1); + } + if (isParagraph) + { + _paragraphStarted = true; + } + else + { + _sentenceStarted = true; + } + return _speakInfo.Voice; + } + + public void EndProcessTextBlock(bool isParagraph) + { + if (isParagraph) + { + _paragraphStarted = true; + } + else + { + _sentenceStarted = true; + } + } + + public void ProcessPhoneme(ref FragmentState fragmentState, AlphabetType alphabet, string ph, char[] phoneIds) + { + fragmentState.Action = TtsEngineAction.Pronounce; + fragmentState.Phoneme = _speakInfo.Voice.TtsEngine.ConvertPhonemes(phoneIds, alphabet); + } + + public void ProcessProsody(string pitch, string range, string rate, string volume, string duration, string points) + { + } + + public void ProcessSayAs(string interpretAs, string format, string detail) + { + } + + public void ProcessSub(string alias, object voice, ref FragmentState fragmentState, int position, bool fIgnore) + { + ProcessText(alias, voice, ref fragmentState, position, fIgnore); + } + + public object ProcessVoice(string name, CultureInfo culture, VoiceGender gender, VoiceAge age, int variant, bool fNewCulture, List extraNamespace) + { + _speakInfo.SetVoice(name, culture, gender, age, variant); + return _speakInfo.Voice; + } + + public void ProcessLexicon(Uri uri, string type) + { + _lexicons.Add(new LexiconEntry(uri, type)); + } + + public void ProcessUnknownElement(object voice, ref FragmentState fragmentState, XmlReader reader) + { + StringWriter sw = new(CultureInfo.InvariantCulture); + XmlTextWriter writer = new(sw); + writer.WriteNode(reader, false); + writer.Close(); + string text = sw.ToString(); + + AddParseUnknownFragment(voice, ref fragmentState, text); + } + + public void StartProcessUnknownAttributes(object voice, ref FragmentState fragmentState, string element, List extraAttributes) + { + StringBuilder sb = new(); + sb.AppendFormat(CultureInfo.InvariantCulture, "<{0}", element); + foreach (SsmlXmlAttribute attribute in extraAttributes) + { + sb.AppendFormat(CultureInfo.InvariantCulture, " {0}:{1}=\"{2}\" xmlns:{3}=\"{4}\"", attribute._prefix, attribute._name, attribute._value, attribute._prefix, attribute._ns); + } + sb.Append('>'); + + AddParseUnknownFragment(voice, ref fragmentState, sb.ToString()); + } + + public void EndProcessUnknownAttributes(object voice, ref FragmentState fragmentState, string element, List extraAttributes) + { + AddParseUnknownFragment(voice, ref fragmentState, string.Format(CultureInfo.InvariantCulture, "", element)); + } + + #region Prompt Engine + + public void ContainsPexml(string pexmlPrefix) + { + } + + public bool BeginPromptEngineOutput(object voice) + { + return false; + } + + public void EndPromptEngineOutput(object voice) + { + } + + public bool ProcessPromptEngineDatabase(object voice, string fname, string delta, string idset) + { + return false; + } + + public bool ProcessPromptEngineDiv(object voice) + { + return false; + } + + public bool ProcessPromptEngineId(object voice, string id) + { + return false; + } + + public bool BeginPromptEngineTts(object voice) + { + return false; + } + + public void EndPromptEngineTts(object voice) + { + } + + public bool BeginPromptEngineWithTag(object voice, string tag) + { + return false; + } + + public void EndPromptEngineWithTag(object voice, string tag) + { + } + + public bool BeginPromptEngineRule(object voice, string name) + { + return false; + } + + public void EndPromptEngineRule(object voice, string name) + { + } + #endregion + + public void EndElement() + { + } + + public void EndSpeakElement() + { + } + + #endregion + + #region Internal Properties + + public string Ssml + { + get + { + return _ssmlText; + } + } + + #endregion + + #region Private Methods + + private static TtsEngineAction ActionTextFragment(TtsEngineAction action) + { + return action; + } + + private void AddParseUnknownFragment(object voice, ref FragmentState fragmentState, string text) + { + TtsEngineAction action = fragmentState.Action; + fragmentState.Action = TtsEngineAction.ParseUnknownTag; + _speakInfo.AddText((TTSVoice)voice, new TextFragment(fragmentState, text)); + fragmentState.Action = action; + } + + #endregion + + #region Private Fields + + private List _lexicons; + private SpeakInfo _speakInfo; + private string _ssmlText; + private bool _paragraphStarted = true; + private bool _sentenceStarted = true; + private ResourceLoader _resourceLoader; + + #endregion + } +} diff --git a/src/libraries/System.Speech/src/Internal/Synthesis/TextWriterEngine.cs b/src/libraries/System.Speech/src/Internal/Synthesis/TextWriterEngine.cs new file mode 100644 index 00000000000000..e33eedcebad857 --- /dev/null +++ b/src/libraries/System.Speech/src/Internal/Synthesis/TextWriterEngine.cs @@ -0,0 +1,385 @@ +// 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.Globalization; +using System.Speech.Synthesis; +using System.Speech.Synthesis.TtsEngine; +using System.Xml; + +#pragma warning disable 56524 // The _xmlWriter member is not created in this module and should not be disposed + +namespace System.Speech.Internal.Synthesis +{ + internal class TextWriterEngine : ISsmlParser + { + #region Constructors + + internal TextWriterEngine(XmlTextWriter writer, CultureInfo culture) + { + _writer = writer; + _culture = culture; + } + + #endregion + + #region Internal Methods + + public object ProcessSpeak(string sVersion, string baseUri, CultureInfo culture, List extraNamespace) + { + if (!string.IsNullOrEmpty(baseUri)) + { + throw new ArgumentException(SR.Get(SRID.InvalidSpeakAttribute, "baseUri", "speak"), nameof(baseUri)); + } + + bool fNewCulture = culture != null && !culture.Equals(_culture); + if (fNewCulture || !string.IsNullOrEmpty(_pexmlPrefix) || extraNamespace.Count > 0) + { + _writer.WriteStartElement("voice"); + + // Always add the culture info as the voice element cannot not be empty (namespaces declaration don't count) + _writer.WriteAttributeString("xml", "lang", null, culture != null ? culture.Name : _culture.Name); + + // write all the additional namespace + foreach (SsmlXmlAttribute ns in extraNamespace) + { + _writer.WriteAttributeString("xmlns", ns._name, ns._ns, ns._value); + } + + // If the prompt builder is used with to add prompt engine data, add the namespace + if (!string.IsNullOrEmpty(_pexmlPrefix)) + { + _writer.WriteAttributeString("xmlns", _pexmlPrefix, null, xmlNamespacePrompt); + } + + _closeSpeak = true; + } + + return null; + } + + public void ProcessText(string text, object voice, ref FragmentState fragmentState, int position, bool fIgnore) + { + _writer.WriteString(text); + } + + public void ProcessAudio(object voice, string uri, string baseUri, bool fIgnore) + { + _writer.WriteStartElement("audio"); + _writer.WriteAttributeString("src", uri); + } + + public void ProcessBreak(object voice, ref FragmentState fragmentState, EmphasisBreak eBreak, int time, bool fIgnore) + { + _writer.WriteStartElement("break"); + if (time > 0 && eBreak == EmphasisBreak.None) + { + _writer.WriteAttributeString("time", time.ToString(CultureInfo.InvariantCulture) + "ms"); + } + else + { + string value = null; + switch (eBreak) + { + case EmphasisBreak.None: + value = "none"; + break; + + case EmphasisBreak.ExtraWeak: + value = "x-weak"; + break; + + case EmphasisBreak.Weak: + value = "weak"; + break; + + case EmphasisBreak.Medium: + value = "medium"; + break; + + case EmphasisBreak.Strong: + value = "strong"; + break; + + case EmphasisBreak.ExtraStrong: + value = "x-strong"; + break; + } + if (!string.IsNullOrEmpty(value)) + { + _writer.WriteAttributeString("strength", value); + } + } + } + + public void ProcessDesc(CultureInfo culture) + { + _writer.WriteStartElement("desc"); + if (culture != null) + { + _writer.WriteAttributeString("xml", "lang", null, culture.Name); + } + } + + public void ProcessEmphasis(bool noLevel, EmphasisWord word) + { + _writer.WriteStartElement("emphasis"); + if (word != EmphasisWord.Default) + { + _writer.WriteAttributeString("level", word.ToString().ToLowerInvariant()); + } + } + + public void ProcessMark(object voice, ref FragmentState fragmentState, string name, bool fIgnore) + { + _writer.WriteStartElement("mark"); + _writer.WriteAttributeString("name", name); + } + + public object ProcessTextBlock(bool isParagraph, object voice, ref FragmentState fragmentState, CultureInfo culture, bool newCulture, VoiceGender gender, VoiceAge age) + { + _writer.WriteStartElement(isParagraph ? "p" : "s"); + if (culture != null) + { + _writer.WriteAttributeString("xml", "lang", null, culture.Name); + } + return null; + } + + public void EndProcessTextBlock(bool isParagraph) + { + } + + public void ProcessPhoneme(ref FragmentState fragmentState, AlphabetType alphabet, string ph, char[] phoneIds) + { + _writer.WriteStartElement("phoneme"); + if (alphabet != AlphabetType.Ipa) + { + _writer.WriteAttributeString("alphabet", alphabet == AlphabetType.Sapi ? "x-microsoft-sapi" : "x-microsoft-ups"); + System.Diagnostics.Debug.Assert(alphabet == AlphabetType.Ups || alphabet == AlphabetType.Sapi); + } + _writer.WriteAttributeString("ph", ph); + } + + public void ProcessProsody(string pitch, string range, string rate, string volume, string duration, string points) + { + _writer.WriteStartElement("prosody"); + if (!string.IsNullOrEmpty(range)) + { + _writer.WriteAttributeString("range", range); + } + if (!string.IsNullOrEmpty(rate)) + { + _writer.WriteAttributeString("rate", rate); + } + if (!string.IsNullOrEmpty(volume)) + { + _writer.WriteAttributeString("volume", volume); + } + if (!string.IsNullOrEmpty(duration)) + { + _writer.WriteAttributeString("duration", duration); + } + if (!string.IsNullOrEmpty(points)) + { + _writer.WriteAttributeString("range", points); + } + } + + public void ProcessSayAs(string interpretAs, string format, string detail) + { + _writer.WriteStartElement("say-as"); + _writer.WriteAttributeString("interpret-as", interpretAs); + if (!string.IsNullOrEmpty(format)) + { + _writer.WriteAttributeString("format", format); + } + if (!string.IsNullOrEmpty(detail)) + { + _writer.WriteAttributeString("detail", detail); + } + } + + public void ProcessSub(string alias, object voice, ref FragmentState fragmentState, int position, bool fIgnore) + { + _writer.WriteStartElement("sub"); + _writer.WriteAttributeString("alias", alias); + } + public object ProcessVoice(string name, CultureInfo culture, VoiceGender gender, VoiceAge age, int variant, bool fNewCulture, List extraNamespace) + { + _writer.WriteStartElement("voice"); + if (!string.IsNullOrEmpty(name)) + { + _writer.WriteAttributeString("name", name); + } + if (fNewCulture && culture != null) + { + _writer.WriteAttributeString("xml", "lang", null, culture.Name); + } + if (gender != VoiceGender.NotSet) + { + _writer.WriteAttributeString("gender", gender.ToString().ToLowerInvariant()); + } + if (age != VoiceAge.NotSet) + { + _writer.WriteAttributeString("age", ((int)age).ToString(CultureInfo.InvariantCulture)); + } + if (variant > 0) + { + _writer.WriteAttributeString("variant", (variant).ToString(CultureInfo.InvariantCulture)); + } + + // write all the additional namespace + if (extraNamespace != null) + { + foreach (SsmlXmlAttribute ns in extraNamespace) + { + _writer.WriteAttributeString("xmlns", ns._name, ns._ns, ns._value); + } + } + return null; + } + + public void ProcessLexicon(Uri uri, string type) + { + _writer.WriteStartElement("lexicon"); + _writer.WriteAttributeString("uri", uri.ToString()); + if (!string.IsNullOrEmpty(type)) + { + _writer.WriteAttributeString("type", type); + } + } + + public void EndElement() + { + _writer.WriteEndElement(); + } + + public void EndSpeakElement() + { + if (_closeSpeak) + { + _writer.WriteEndElement(); + } + } + + public void ProcessUnknownElement(object voice, ref FragmentState fragmentState, XmlReader reader) + { + _writer.WriteNode(reader, false); + } + + public void StartProcessUnknownAttributes(object voice, ref FragmentState fragmentState, string sElement, List extraAttributes) + { + // write all the additional namespace + foreach (SsmlXmlAttribute attribute in extraAttributes) + { + _writer.WriteAttributeString(attribute._prefix, attribute._name, attribute._ns, attribute._value); + } + } + + public void EndProcessUnknownAttributes(object voice, ref FragmentState fragmentState, string sElement, List extraAttributes) + { + } + + #region Prompt Engine + + public void ContainsPexml(string pexmlPrefix) + { + _pexmlPrefix = pexmlPrefix; + } + + private bool ProcessPromptEngine(string element, params KeyValuePair[] attributes) + { + _writer.WriteStartElement(_pexmlPrefix, element, xmlNamespacePrompt); + + if (attributes != null) + { + foreach (KeyValuePair kp in attributes) + { + if (kp.Value != null) + { + _writer.WriteAttributeString(kp.Key, kp.Value); + } + } + } + return true; + } + + public bool BeginPromptEngineOutput(object voice) + { + return ProcessPromptEngine("prompt_output"); + } + + public bool ProcessPromptEngineDatabase(object voice, string fname, string delta, string idset) + { + return ProcessPromptEngine("database", new KeyValuePair[] { new KeyValuePair("fname", fname), new KeyValuePair("delta", delta), new KeyValuePair("idset", idset) }); + } + + public bool ProcessPromptEngineDiv(object voice) + { + return ProcessPromptEngine("div"); + } + + public bool ProcessPromptEngineId(object voice, string id) + { + return ProcessPromptEngine("id", new KeyValuePair[] { new KeyValuePair("id", id) }); + } + + public bool BeginPromptEngineTts(object voice) + { + return ProcessPromptEngine("tts"); + } + + public void EndPromptEngineTts(object voice) + { + } + + public bool BeginPromptEngineWithTag(object voice, string tag) + { + return ProcessPromptEngine("withtag", new KeyValuePair[] { new KeyValuePair("tag", tag) }); + } + + public void EndPromptEngineWithTag(object voice, string tag) + { + } + + public bool BeginPromptEngineRule(object voice, string name) + { + return ProcessPromptEngine("rule", new KeyValuePair[] { new KeyValuePair("name", name) }); + } + + public void EndPromptEngineRule(object voice, string name) + { + } + + public void EndPromptEngineOutput(object voice) + { + } + + #endregion + + #endregion + + #region Internal Properties + + public string Ssml + { + get + { + return null; + } + } + + #endregion + + #region Private Fields + + private XmlTextWriter _writer; + private CultureInfo _culture; + private bool _closeSpeak; + private string _pexmlPrefix; + private const string xmlNamespacePrompt = "http://schemas.microsoft.com/Speech/2003/03/PromptEngine"; + + #endregion + } +} diff --git a/src/libraries/System.Speech/src/Internal/Synthesis/VoiceSynthesis.cs b/src/libraries/System.Speech/src/Internal/Synthesis/VoiceSynthesis.cs new file mode 100644 index 00000000000000..b45768358f9037 --- /dev/null +++ b/src/libraries/System.Speech/src/Internal/Synthesis/VoiceSynthesis.cs @@ -0,0 +1,1853 @@ +// 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.Diagnostics; +using System.Globalization; +using System.IO; +using System.Reflection; +using System.Runtime.ExceptionServices; +using System.Runtime.InteropServices; +using System.Speech.AudioFormat; +using System.Speech.Internal.ObjectTokens; +using System.Speech.Synthesis; +using System.Speech.Synthesis.TtsEngine; +using System.Text; +using System.Threading; + +#pragma warning disable 56502 // Empty catch statements + +namespace System.Speech.Internal.Synthesis +{ + internal sealed class VoiceSynthesis : IDisposable + { + #region Constructors + + internal VoiceSynthesis(WeakReference speechSynthesizer) + { + _asyncWorker = new AsyncSerializedWorker(new WaitCallback(ProcessPostData), null); + _asyncWorkerUI = new AsyncSerializedWorker(null, SynchronizationContext.Current); + + // 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) + _waveOut = new AudioDeviceOut(SAPICategories.DefaultDeviceOut(), _asyncWorker); + + // Build the installed voice collection on first run + if (s_allVoices == null) + { + s_allVoices = BuildInstalledVoices(this); + + // If no voice are installed, then bail out. + if (s_allVoices.Count == 0) + { + s_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(s_allVoices.Count); + foreach (InstalledVoice installedVoice in s_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)) + { + 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 + + #region Internal Methods + + #region SpeechSynthesis 'public' API implementation + 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 occurred + if (prompt.Exception != null) + { + ExceptionDispatchInfo.Throw(prompt.Exception); + } + } + finally + { + _asyncWorkerUI.AsyncMode = true; + _asyncWorkerUI.WorkItemPending -= _signalWorkerCallback; + _stateChanged -= eventHandler; + } + } + 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 substitution 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(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); + } + } + + 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); + } + } + + 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, nameof(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, nameof(eventHandler)); + + // could through if unsuccessful - delay the SetEventInterest + internalEventHandler -= eventHandler; + + if (internalEventHandler == null) + { + _ttsEvents &= ~(1 << (int)ttsEvent); + + SetInterest(_ttsEvents); + } + } + } + + #endregion + + #endregion + 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) + { + _waveOut = new AudioDeviceOut(SAPICategories.DefaultDeviceOut(), _asyncWorker); + } + 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(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(); + + // 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); + } + } + + #endregion + + #region Internal Properties + internal Prompt Prompt + { + get + { + lock (_pendingSpeakQueue) + { + return _currentPrompt; + } + } + } + internal SynthesizerState State + { + get + { + return _synthesizerState; + } + } + internal int Rate + { + get + { + return _site.VoiceRate; + } + set + { + _site.VoiceRate = _defaultRate = value; + } + } + internal int Volume + { + get + { + return _site.VoiceVolume; + } + set + { + _site.VoiceVolume = value; + } + } + + /// + /// 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; + } + } + + #endregion + + #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; + + internal EventHandler _phonemeReached; + + internal EventHandler _visemeReached; + + #endregion + + #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. + /// + private 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(); ; + + //--- Create a single speak info structure for all the text + TTSVoice voice = _currentVoice != null ? _currentVoice : GetVoice(false); + //--- Create the speak info + + SpeakInfo speakInfo = new(this, voice); + + if (paramSpeak._textToSpeak != null) + { + //--- Make sure we have a voice defined by now + if (!paramSpeak._isXml) + { + FragmentState fragmentState = new(); + fragmentState.Action = TtsEngineAction.Speak; + fragmentState.Prosody = new Prosody(); + TextFragment textFragment = new(fragmentState, paramSpeak._textToSpeak); + speakInfo.AddText(voice, textFragment); + } + else + { + TextFragmentEngine engine = new(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 + + 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 segments, 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; + + // 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; + } + // 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; + ExceptionDispatchInfo.Throw(lastException); + } + //--- Always inject the end of stream and complete event on failure + throw new OperationCanceledException(SR.Get(SRID.PromptAsyncOperationCancelled)); + } + } + } + } + } + + /// + /// Get the user's default rate from the registry + /// + private static uint GetDefaultRate() + { + //--- Read the current user's default rate + uint 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(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 occurred + 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(_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 instantiated. + // Remove it from the list of possible 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 over 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); + } + 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.Open(null, voiceInfo.RegistryKeyPath, false); + if (token != null) + { + object engine = token.CreateObjectFromToken("CLSID"); + + if (engine != null) + { + ITtsEngine iTtsEngine = engine as ITtsEngine; + if (iTtsEngine != null) + { + engineProxy = new TtsProxySapi(iTtsEngine, ComEngineSite, voiceInfo.Culture.LCID); + } + } + } + } + catch (ArgumentException) + { + } + catch (IOException) + { + } + catch (BadImageFormatException) + { + } + catch (COMException) + { + } + catch (FormatException) + { + } + return engineProxy; + } + + /// + /// Returns the default voice for the synth + /// + private TTSVoice GetVoice(bool switchContext) + { + lock (_defaultVoiceLock) + { + if (!_defaultVoiceInitialized) + { + _defaultVoice = null; + ObjectToken defaultVoice = SAPICategories.DefaultToken("Voices"); + + if (defaultVoice != null) + { + // Try to load a default voice from the default token parameters + VoiceGender gender = VoiceGender.NotSet; + VoiceAge age = VoiceAge.NotSet; + SsmlParserHelpers.TryConvertGender(defaultVoice.Gender.ToLowerInvariant(), out gender); + SsmlParserHelpers.TryConvertAge(defaultVoice.Age.ToLowerInvariant(), out age); + + _defaultVoice = GetEngineWithVoice(null, new VoiceInfo(defaultVoice), defaultVoice.TokenName(), defaultVoice.Culture, gender, age, 1, switchContext); + + // If failed to get the default, then reset the default token to null. + defaultVoice = null; + } + + if (_defaultVoice == null) + { + // Try to find a default voice that matches the current UI culture + VoiceInfo defaultInfo = defaultVoice != null ? new VoiceInfo(defaultVoice) : null; + _defaultVoice = GetEngineWithVoice(null, defaultInfo, null, CultureInfo.CurrentUICulture, VoiceGender.NotSet, VoiceAge.NotSet, 1, switchContext); + } + _defaultVoiceInitialized = true; + _currentVoice = _defaultVoice; + } + } + return _defaultVoice; + } + + private static List BuildInstalledVoices(VoiceSynthesis voiceSynthesizer) + { + List voices = new(); + + using (ObjectTokenCategory category = ObjectTokenCategory.Create(SAPICategories.Voices)) + { + if (category != null) + { + // Build a list with all the voicesInfo + foreach (ObjectToken voiceToken in category.FindMatchingTokens(null, null)) + { + if (voiceToken != null && voiceToken.Attributes != null) + { + voices.Add(new InstalledVoice(voiceSynthesizer, new VoiceInfo(voiceToken))); + } + } + } + } + return voices; + } + + #region Signal Client application + + private void SignalWorkerThread(object ignored) + { + if (_asyncWorkerUI.AsyncMode == false) + { + _workerWaitHandle.Set(); + } + } + + private void ProcessPostData(object arg) + { + TTSEvent ttsEvent = arg as TTSEvent; + if (ttsEvent == null) + { + Debug.WriteLine("ProcessPostData: post data is not a TTSEvent object"); + return; + } + lock (_thisObjectLock) + { + if (!_isDisposed) + { + DispatchEvent(ttsEvent); + } + } + } + + private void DispatchEvent(TTSEvent ttsEvent) + { + Prompt prompt = ttsEvent.Prompt; + Debug.Assert(prompt != null); + + // Raise the appropriate events + TtsEventId eventId = ttsEvent.Id; + prompt.Exception = ttsEvent.Exception; + switch (eventId) + { + case TtsEventId.StartInputStream: + // SpeakStarted + OnSpeakStarted(new SpeakStartedEventArgs(prompt)); + break; + + case TtsEventId.EndInputStream: + // SpeakCompleted + OnSpeakCompleted(new SpeakCompletedEventArgs(prompt)); + break; + + case TtsEventId.SentenceBoundary: + break; + + case TtsEventId.WordBoundary: + // SpeakProgressChanged + OnSpeakProgress(new SpeakProgressEventArgs(prompt, ttsEvent.AudioPosition, (int)ttsEvent.LParam, (int)ttsEvent.WParam)); + break; + + case TtsEventId.Bookmark: + // BookmarkDetected + OnBookmarkReached(new BookmarkReachedEventArgs(prompt, ttsEvent.Bookmark, ttsEvent.AudioPosition)); + break; + + case TtsEventId.VoiceChange: + VoiceInfo voice = ttsEvent.Voice; + OnVoiceChange(new VoiceChangeEventArgs(prompt, voice)); + break; + + case TtsEventId.Phoneme: + // SynthesizePhoneme + OnPhonemeReached(new PhonemeReachedEventArgs( + prompt, // Prompt + ttsEvent.Phoneme, // Current phoneme + ttsEvent.AudioPosition, // audioPosition + ttsEvent.PhonemeDuration, + ttsEvent.PhonemeEmphasis, + ttsEvent.NextPhoneme)); // next phoneme + break; + + case TtsEventId.Viseme: + // SynthesizeViseme + OnVisemeReached(new VisemeReachedEventArgs( + prompt, // Prompt + (int)ttsEvent.LParam & 0xFFFF, // currentViseme + ttsEvent.AudioPosition, // audioPosition + TimeSpan.FromMilliseconds(ttsEvent.WParam >> 16), // duration + (SynthesizerEmphasis)((uint)ttsEvent.LParam >> 16), // Emphasis + (int)(ttsEvent.WParam & 0xFFFF))); // nextViseme + break; + + default: + throw new InvalidOperationException(SR.Get(SRID.SynthesizerUnknownEvent)); + } + } + + #endregion + + private void Dispose(bool disposing) + { + if (!_isDisposed) + { + lock (_thisObjectLock) + { + _fExitWorkerThread = true; + + // Wait for 2 second max for any pending speak + Abort(); + for (int i = 0; i < 20 && State != SynthesizerState.Ready; i++) + { + Thread.Sleep(100); + } + if (disposing) + { + _evtPendingSpeak.Set(); + + // Wait for the background thread to be done. + _workerThread.Join(); + + // Free the COM resources used + foreach (KeyValuePair kv in _voiceDictionary) + { + if (kv.Value != null) + { + kv.Value.TtsEngine.ReleaseInterface(); + } + } + _voiceDictionary.Clear(); + + _evtPendingSpeak.Close(); + _evtPendingGetProxy.Close(); + _workerWaitHandle.Close(); + } + + // If the TTS engine was a COM object, release it. + if (_iSite != IntPtr.Zero) + { + Marshal.Release(_iSite); + } + + // Mark this object as disposed + _isDisposed = true; + } + } + } + private void QueuePrompt(Prompt prompt) + { + // Call Sapi Speak with the appropriate flags based on mediaType + switch (prompt._media) + { + case SynthesisMediaType.Text: + // Synthesize the speech based on plain text + Speak(prompt._text, prompt, false); + break; + + case SynthesisMediaType.Ssml: + // Synthesize the speech based on Ssml input + Speak(prompt._text, prompt, true); + break; + + case SynthesisMediaType.WaveAudio: + // Synthesize the speech based for Audio + SpeakStream(prompt._audio, prompt); + break; + + default: + throw new ArgumentException(SR.Get(SRID.SynthesizerUnknownMediaType)); + } + } + + /// + /// This method is used to speak a text buffer. + /// + private void Speak(string textToSpeak, Prompt prompt, bool fIsXml) + { + Helpers.ThrowIfNull(textToSpeak, nameof(textToSpeak)); + + if (_isDisposed) + { + throw new ObjectDisposedException("VoiceSynthesis"); + } + + //--- Add the Speak info to the pending TTS rendering list + AddSpeakParameters(new Parameters(Action.SpeakText, new ParametersSpeak(textToSpeak, prompt, fIsXml, null))); + } + + private void SpeakStream(Uri audio, Prompt prompt) + { + //--- Add the Speak info to the pending TTS rendering list + AddSpeakParameters(new Parameters(Action.SpeakText, new ParametersSpeak(null, prompt, false, audio))); + } + private void SetInterest(int ttsInterest) + { + _ttsInterest = ttsInterest; + //--- Purge all pending speak requests and reset the voice + lock (_pendingSpeakQueue) + { + _site.SetEventsInterest(_ttsInterest); + } + } + + #endregion + + #region Private Properties + + private IntPtr ComEngineSite + { + get + { + // Get the local EngineSite as a COM component + if (_iSite == IntPtr.Zero) + { + _siteSapi = new EngineSiteSapi(_site, _resourceLoader); + _iSite = Marshal.GetComInterfaceForObject(_siteSapi, typeof(ISpEngineSite)); + } + return _iSite; + } + } + + #endregion + + #region Private Types + +#pragma warning disable 56524 // No instances of a class created in this module and should not be disposed + + private enum Action + { + GetVoice, + SpeakText, + } + + private class Parameters + { + internal Parameters(Action action, object parameter) + { + _action = action; + _parameter = parameter; + } + + internal Action _action; + internal object _parameter; + } + + private class ParametersSpeak + { + internal ParametersSpeak(string textToSpeak, Prompt prompt, bool isXml, Uri audioFile) + { + _textToSpeak = textToSpeak; + _prompt = prompt; + _isXml = isXml; + _audioFile = audioFile; + } + + internal string _textToSpeak; + internal Prompt _prompt; + internal bool _isXml; + internal Uri _audioFile; + } + +#pragma warning restore 56524 // No instances of a class created in this module and should not be disposed + + #endregion + + #region Private Fields + + // Notifications + private WaitCallback _eventStateChanged; + private WaitCallback _signalWorkerCallback; + + // Engine site references + private readonly ResourceLoader _resourceLoader; + private readonly EngineSite _site; + private EngineSiteSapi _siteSapi; + private IntPtr _iSite; + private int _ttsInterest; + + // Background synchronization + private ManualResetEvent _evtPendingSpeak = new(false); + private ManualResetEvent _evtPendingGetProxy = new(false); + private Exception _pendingException; + private Queue _pendingSpeakQueue = new(); + private TTSVoice _pendingVoice; + + // Background thread + private Thread _workerThread; + private bool _fExitWorkerThread; + private object _processingSpeakLock = new(); + + // Voices info + private Dictionary _voiceDictionary = new(); + private List _installedVoices; + private static List s_allVoices; + private object _enabledVoicesLock = new(); + + // Default voice + private TTSVoice _defaultVoice; + private TTSVoice _currentVoice; + private bool _defaultVoiceInitialized; + private object _defaultVoiceLock = new(); + + private AudioBase _waveOut; + private int _defaultRate; + + // Is the object disposed? + private bool _isDisposed; + + // Lexicons associated with this voice + private List _lexicons = new(); + + // output object + private SynthesizerState _synthesizerState = SynthesizerState.Ready; + + // Currently played prompt + private Prompt _currentPrompt; + + private const string defaultVoiceRate = "DefaultTTSRate"; + + private AsyncSerializedWorker _asyncWorker, _asyncWorkerUI; + + // Prompt Engine + private const bool _pexml = false; + + /// + /// Could be a phrase of an SSML doc or a file reference + /// + private int _ttsEvents = (1 << (int)TtsEventId.StartInputStream) | (1 << (int)TtsEventId.EndInputStream); + + // make sure the object is always in safe state + private object _thisObjectLock = new(); + + private AutoResetEvent _workerWaitHandle = new(false); + + private WeakReference _speechSyntesizer; + + private readonly string[] _xmlEscapeStrings = new string[] { """, "'", "&", "<", ">" }; + private readonly char[] _xmlEscapeChars = new char[] { '"', '\'', '&', '<', '>' }; + + #endregion + } +} diff --git a/src/libraries/System.Speech/src/Internal/Synthesis/WaveHeader.cs b/src/libraries/System.Speech/src/Internal/Synthesis/WaveHeader.cs new file mode 100644 index 00000000000000..ea569ba86ff350 --- /dev/null +++ b/src/libraries/System.Speech/src/Internal/Synthesis/WaveHeader.cs @@ -0,0 +1,154 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Runtime.InteropServices; + +namespace System.Speech.Internal.Synthesis +{ + + internal sealed class WaveHeader : IDisposable + { + #region Constructors + + /// + /// Initialize an instance of a byte array. + /// + /// MMSYSERR.NOERROR if successful + internal WaveHeader(byte[] buffer) + { + _dwBufferLength = buffer.Length; + _gcHandle = GCHandle.Alloc(buffer, GCHandleType.Pinned); + } + + /// + /// Frees any memory allocated for the buffer. + /// + ~WaveHeader() + { + Dispose(false); + } + public void Dispose() + { + Dispose(true); + GC.SuppressFinalize(this); + } + + /// + /// Frees any memory allocated for the buffer. + /// + private void Dispose(bool disposing) + { + if (disposing) + { + ReleaseData(); + if (_gcHandleWaveHdr.IsAllocated) + { + _gcHandleWaveHdr.Free(); + } + } + } + + #endregion + + #region Internal Methods + + internal void ReleaseData() + { + if (_gcHandle.IsAllocated) + { + _gcHandle.Free(); + } + } + + #endregion + + #region Internal Properties + internal GCHandle WAVEHDR + { + get + { + if (!_gcHandleWaveHdr.IsAllocated) + { + _waveHdr.lpData = _gcHandle.AddrOfPinnedObject(); + _waveHdr.dwBufferLength = (uint)_dwBufferLength; + _waveHdr.dwBytesRecorded = 0; + _waveHdr.dwUser = 0; + _waveHdr.dwFlags = 0; + _waveHdr.dwLoops = 0; + _waveHdr.lpNext = IntPtr.Zero; + _gcHandleWaveHdr = GCHandle.Alloc(_waveHdr, GCHandleType.Pinned); + } + return _gcHandleWaveHdr; + } + } + + internal int SizeHDR + { + get + { + return Marshal.SizeOf(_waveHdr); + } + } + + #endregion + + #region Internal Fields + + /// + /// Used by dwFlags in WaveHeader + /// Set by the device driver to indicate that it is finished with the buffer + /// and is returning it to the application. + /// + internal const int WHDR_DONE = 0x00000001; + /// + /// Used by dwFlags in WaveHeader + /// Set by Windows to indicate that the buffer has been prepared with the + /// waveInPrepareHeader or waveOutPrepareHeader function. + /// + internal const int WHDR_PREPARED = 0x00000002; + /// + /// Used by dwFlags in WaveHeader + /// This buffer is the first buffer in a loop. This flag is used only with + /// output buffers. + /// + internal const int WHDR_BEGINLOOP = 0x00000004; + /// + /// Used by dwFlags in WaveHeader + /// This buffer is the last buffer in a loop. This flag is used only with + /// output buffers. + /// + internal const int WHDR_ENDLOOP = 0x00000008; + /// + /// Used by dwFlags in WaveHeader + /// Set by Windows to indicate that the buffer is queued for playback. + /// + internal const int WHDR_INQUEUE = 0x00000010; + + /// + /// Set in WaveFormat.wFormatTag to specify PCM data. + /// + internal const int WAVE_FORMAT_PCM = 1; + + #endregion + + #region private Fields + + /// + /// Long pointer to the address of the waveform buffer. This buffer must + /// be block-aligned according to the nBlockAlign member of the + /// WaveFormat structure used to open the device. + /// + private GCHandle _gcHandle; + + private GCHandle _gcHandleWaveHdr; + + private WAVEHDR _waveHdr; + + /// + /// Specifies the length, in bytes, of the buffer. + /// + internal int _dwBufferLength; + + #endregion + } +} diff --git a/src/libraries/System.Speech/src/Recognition/AudioLevelUpdatedEventArgs.cs b/src/libraries/System.Speech/src/Recognition/AudioLevelUpdatedEventArgs.cs new file mode 100644 index 00000000000000..572a393a85a5bb --- /dev/null +++ b/src/libraries/System.Speech/src/Recognition/AudioLevelUpdatedEventArgs.cs @@ -0,0 +1,33 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace System.Speech.Recognition +{ + // EventArgs used in the AudioLevelUpdatedEventArgs event. + + public class AudioLevelUpdatedEventArgs : EventArgs + { + #region Constructors + + internal AudioLevelUpdatedEventArgs(int audioLevel) + { + _audioLevel = audioLevel; + } + + #endregion + + #region public Properties + public int AudioLevel + { + get { return _audioLevel; } + } + + #endregion + + #region Private Fields + + private int _audioLevel; + + #endregion + } +} diff --git a/src/libraries/System.Speech/src/Recognition/AudioSignalProblem.cs b/src/libraries/System.Speech/src/Recognition/AudioSignalProblem.cs new file mode 100644 index 00000000000000..d75bae5da19169 --- /dev/null +++ b/src/libraries/System.Speech/src/Recognition/AudioSignalProblem.cs @@ -0,0 +1,29 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace System.Speech.Recognition +{ + public enum AudioSignalProblem + { + // No signal problem. + None = 0, + + // The audio input is too noisy for accurate recognition of the input phrase. + TooNoisy, + + // The audio input does not contain any audio signal (flat line). + NoSignal, + + // The audio input is too loud, resulting in clipping of the signal. + TooLoud, + + // The audio input is too soft, resulting in sub-optimal recognition of the input phrase. + TooSoft, + + // The audio input is too fast for optimal recognition. + TooFast, + + // The audio input is too slow for optimal recognition. + TooSlow + } +} diff --git a/src/libraries/System.Speech/src/Recognition/AudioSignalProblemOccurredEventArgs.cs b/src/libraries/System.Speech/src/Recognition/AudioSignalProblemOccurredEventArgs.cs new file mode 100644 index 00000000000000..317d290f951b25 --- /dev/null +++ b/src/libraries/System.Speech/src/Recognition/AudioSignalProblemOccurredEventArgs.cs @@ -0,0 +1,51 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace System.Speech.Recognition +{ + // EventArgs used in the AudioSignalProblemOccurredEventArgs event. + + public class AudioSignalProblemOccurredEventArgs : EventArgs + { + #region Constructors + + internal AudioSignalProblemOccurredEventArgs(AudioSignalProblem audioSignalProblem, int audioLevel, TimeSpan audioPosition, TimeSpan recognizerPosition) + { + _audioSignalProblem = audioSignalProblem; + _audioLevel = audioLevel; + _audioPosition = audioPosition; + _recognizerPosition = recognizerPosition; + } + + #endregion + + #region public Properties + public AudioSignalProblem AudioSignalProblem + { + get { return _audioSignalProblem; } + } + public int AudioLevel + { + get { return _audioLevel; } + } + public TimeSpan AudioPosition + { + get { return _audioPosition; } + } + public TimeSpan RecognizerAudioPosition + { + get { return _recognizerPosition; } + } + + #endregion + + #region Private Fields + + private AudioSignalProblem _audioSignalProblem; + private TimeSpan _recognizerPosition; + private TimeSpan _audioPosition; + private int _audioLevel; + + #endregion + } +} diff --git a/src/libraries/System.Speech/src/Recognition/AudioState.cs b/src/libraries/System.Speech/src/Recognition/AudioState.cs new file mode 100644 index 00000000000000..2a44ec5b3728c1 --- /dev/null +++ b/src/libraries/System.Speech/src/Recognition/AudioState.cs @@ -0,0 +1,18 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace System.Speech.Recognition +{ + // Current audio state. + public enum AudioState + { + // The audio input is stopped. + Stopped, + + // The audio input contains silence. + Silence, + + // The audio input contains speech signal. + Speech + } +} diff --git a/src/libraries/System.Speech/src/Recognition/AudioStateChangedEventArgs.cs b/src/libraries/System.Speech/src/Recognition/AudioStateChangedEventArgs.cs new file mode 100644 index 00000000000000..6128ab245c1497 --- /dev/null +++ b/src/libraries/System.Speech/src/Recognition/AudioStateChangedEventArgs.cs @@ -0,0 +1,33 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace System.Speech.Recognition +{ + // EventArgs used in the AudioStateChangedEventArgs event. + + public class AudioStateChangedEventArgs : EventArgs + { + #region Constructors + + internal AudioStateChangedEventArgs(AudioState audioState) + { + _audioState = audioState; + } + + #endregion + + #region public Properties + public AudioState AudioState + { + get { return _audioState; } + } + + #endregion + + #region Private Fields + + private AudioState _audioState; + + #endregion + } +} diff --git a/src/libraries/System.Speech/src/Recognition/Choices.cs b/src/libraries/System.Speech/src/Recognition/Choices.cs new file mode 100644 index 00000000000000..86884acea7df97 --- /dev/null +++ b/src/libraries/System.Speech/src/Recognition/Choices.cs @@ -0,0 +1,85 @@ +// 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; +using System.Speech.Internal; +using System.Speech.Internal.GrammarBuilding; + +namespace System.Speech.Recognition +{ + [DebuggerDisplay("{_oneOf.DebugSummary}")] + public class Choices + { + #region Constructors + + public Choices() + { + } + + public Choices(params string[] phrases) + { + Helpers.ThrowIfNull(phrases, nameof(phrases)); + + Add(phrases); + } + + public Choices(params GrammarBuilder[] alternateChoices) + { + Helpers.ThrowIfNull(alternateChoices, nameof(alternateChoices)); + + Add(alternateChoices); + } + + #endregion + + #region Public Methods + + public void Add(params string[] phrases) + { + Helpers.ThrowIfNull(phrases, nameof(phrases)); + + foreach (string phrase in phrases) + { + Helpers.ThrowIfEmptyOrNull(phrase, "phrase"); + + _oneOf.Add(phrase); + } + } + + public void Add(params GrammarBuilder[] alternateChoices) + { + Helpers.ThrowIfNull(alternateChoices, nameof(alternateChoices)); + + foreach (GrammarBuilder alternateChoice in alternateChoices) + { + Helpers.ThrowIfNull(alternateChoice, "alternateChoice"); + + _oneOf.Items.Add(new ItemElement(alternateChoice)); + } + } + public GrammarBuilder ToGrammarBuilder() + { + return new GrammarBuilder(this); + } + + #endregion + + #region Internal Properties + + internal OneOfElement OneOf + { + get + { + return _oneOf; + } + } + + #endregion + + #region Private Fields + + private OneOfElement _oneOf = new(); + + #endregion + } +} diff --git a/src/libraries/System.Speech/src/Recognition/DictationGrammar.cs b/src/libraries/System.Speech/src/Recognition/DictationGrammar.cs new file mode 100644 index 00000000000000..758da033e35356 --- /dev/null +++ b/src/libraries/System.Speech/src/Recognition/DictationGrammar.cs @@ -0,0 +1,57 @@ +// 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; + +namespace System.Speech.Recognition +{ + // Class for grammars based on a statistical language model for doing dictation. + + public class DictationGrammar : Grammar + { + // The implementation of DictationGrammar stores a Uri in the Grammar.Uri field. + // Then when LoadGrammar is called the Uri handling part of LoadGrammar is modified to check + // if the grammar object is a DictationGrammar, in which case the SAPI dictation methods are called. + // The Uri is "grammar:dictation" for regular dictation and "grammar:dictation#spelling" for a spelling. + + #region Constructors + + // Load the generic dictation language model. + public DictationGrammar() : base(s_defaultDictationUri, null, null) + { + } + + // Load a specific topic. The topic is of the form "grammar:dictation#topic" + public DictationGrammar(string topic) : base(new Uri(topic, UriKind.RelativeOrAbsolute), null, null) + { + } + + #endregion + + #region Public Methods + public void SetDictationContext(string precedingText, string subsequentText) + { + if (State != GrammarState.Loaded) + { + throw new InvalidOperationException(SR.Get(SRID.GrammarNotLoaded)); + } + // Note: You can only call this method after the Grammar is Loaded. + // In theory we could support this more generally but there doesn't seem to be a lot of point. + Debug.Assert(Recognizer != null); + + Recognizer.SetDictationContext(this, precedingText, subsequentText); + } + + #endregion + + #region Internal Methods + + #endregion + + #region Private Fields + + private static Uri s_defaultDictationUri = new("grammar:dictation"); + + #endregion + } +} diff --git a/src/libraries/System.Speech/src/Recognition/EmulateRecognizeCompletedEventArgs.cs b/src/libraries/System.Speech/src/Recognition/EmulateRecognizeCompletedEventArgs.cs new file mode 100644 index 00000000000000..68320b64330458 --- /dev/null +++ b/src/libraries/System.Speech/src/Recognition/EmulateRecognizeCompletedEventArgs.cs @@ -0,0 +1,34 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.ComponentModel; + +namespace System.Speech.Recognition +{ + public class EmulateRecognizeCompletedEventArgs : AsyncCompletedEventArgs + { + #region Constructors + + internal EmulateRecognizeCompletedEventArgs(RecognitionResult result, Exception error, bool cancelled, object userState) + : base(error, cancelled, userState) + { + _result = result; + } + + #endregion + + #region Public Properties + public RecognitionResult Result + { + get { return _result; } + } + + #endregion + + #region Private Fields + + private RecognitionResult _result; + + #endregion + } +} diff --git a/src/libraries/System.Speech/src/Recognition/Grammar.cs b/src/libraries/System.Speech/src/Recognition/Grammar.cs new file mode 100644 index 00000000000000..827fc8b56fd5c4 --- /dev/null +++ b/src/libraries/System.Speech/src/Recognition/Grammar.cs @@ -0,0 +1,1166 @@ +// 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.ComponentModel; +using System.Diagnostics; +using System.Globalization; +using System.IO; +using System.Reflection; +using System.Runtime.ExceptionServices; +using System.Speech.Internal; +using System.Speech.Internal.SrgsCompiler; +using System.Speech.Recognition.SrgsGrammar; +using System.Text; + +#pragma warning disable 56500 // Remove all the catch all statements warnings used by the interop layer + +namespace System.Speech.Recognition +{ + // Class for grammars which are to be loaded from SRGS or CFG. + // In contrast to dictation grammars which inherit from this. + [DebuggerDisplay("Grammar: {(_uri != null ? \"uri=\" + _uri.ToString () + \" \" : \"\") + \"rule=\" + _ruleName }")] + public class Grammar + { + #region Constructors + +#pragma warning disable 6504 +#pragma warning disable 6507 + internal Grammar(Uri uri, string ruleName, object[] parameters) + { + Helpers.ThrowIfNull(uri, nameof(uri)); + + _uri = uri; + InitialGrammarLoad(ruleName, parameters, false); + } + public Grammar(string path) + : this(path, (string)null, null) + { + } + public Grammar(string path, string ruleName) + : this(path, ruleName, null) + { + } + public Grammar(string path, string ruleName, object[] parameters) + { + try + { + _uri = new Uri(path, UriKind.Relative); + } + catch (UriFormatException e) + { + throw new ArgumentException(SR.Get(SRID.RecognizerGrammarNotFound), nameof(path), e); + } + + InitialGrammarLoad(ruleName, parameters, false); + } + public Grammar(SrgsDocument srgsDocument) + : this(srgsDocument, null, null, null) + { + } + public Grammar(SrgsDocument srgsDocument, string ruleName) + : this(srgsDocument, ruleName, null, null) + { + } + public Grammar(SrgsDocument srgsDocument, string ruleName, object[] parameters) + : this(srgsDocument, ruleName, null, parameters) + { + } + [EditorBrowsable(EditorBrowsableState.Advanced)] + public Grammar(SrgsDocument srgsDocument, string ruleName, Uri baseUri) + : this(srgsDocument, ruleName, baseUri, null) + { + } + [EditorBrowsable(EditorBrowsableState.Advanced)] + public Grammar(SrgsDocument srgsDocument, string ruleName, Uri baseUri, object[] parameters) + { + Helpers.ThrowIfNull(srgsDocument, nameof(srgsDocument)); + + _srgsDocument = srgsDocument; + _isSrgsDocument = srgsDocument != null; + _baseUri = baseUri; + InitialGrammarLoad(ruleName, parameters, false); + } + public Grammar(Stream stream) + : this(stream, null, null, null) + { + } + public Grammar(Stream stream, string ruleName) + : this(stream, ruleName, null, null) + { + } + public Grammar(Stream stream, string ruleName, object[] parameters) + : this(stream, ruleName, null, parameters) + { + } + [EditorBrowsable(EditorBrowsableState.Advanced)] + public Grammar(Stream stream, string ruleName, Uri baseUri) + : this(stream, ruleName, baseUri, null) + { + } + [EditorBrowsable(EditorBrowsableState.Advanced)] + public Grammar(Stream stream, string ruleName, Uri baseUri, object[] parameters) + { + Helpers.ThrowIfNull(stream, nameof(stream)); + + if (!stream.CanRead) + { + throw new ArgumentException(SR.Get(SRID.StreamMustBeReadable), nameof(stream)); + } + _appStream = stream; + _baseUri = baseUri; + InitialGrammarLoad(ruleName, parameters, false); + } + + public Grammar(GrammarBuilder builder) + { + Helpers.ThrowIfNull(builder, nameof(builder)); + + _grammarBuilder = builder; + InitialGrammarLoad(null, null, false); + } + + private Grammar(string onInitParameters, Stream stream, string ruleName) + { + _appStream = stream; + _onInitParameters = onInitParameters; + InitialGrammarLoad(ruleName, null, true); + } + protected Grammar() + { + } + protected void StgInit(object[] parameters) + { + _parameters = parameters; + LoadAndCompileCfgData(false, true); + } + +#pragma warning restore 6504 +#pragma warning restore 6507 + + #endregion + + #region Public Methods + public static Grammar LoadLocalizedGrammarFromType(Type type, params object[] onInitParameters) + { + Helpers.ThrowIfNull(type, nameof(type)); + + if (type == typeof(Grammar) || !type.IsSubclassOf(typeof(Grammar))) + { + throw new ArgumentException(SR.Get(SRID.StrongTypedGrammarNotAGrammar), nameof(type)); + } + + Assembly assembly = Assembly.GetAssembly(type); + + foreach (Type typeTarget in assembly.GetTypes()) + { + string cultureId = null; + if (typeTarget == type || typeTarget.IsSubclassOf(type)) + { + if (typeTarget.GetField("__cultureId") != null) + { + // Get the association table + try + { + cultureId = (string)typeTarget.InvokeMember("__cultureId", BindingFlags.GetField, null, null, null, null); + } + catch (Exception e) + { + if (!(e is System.MissingFieldException)) + { + throw; + } + } + if (Helpers.CompareInvariantCulture(new CultureInfo(int.Parse(cultureId, CultureInfo.InvariantCulture)), CultureInfo.CurrentUICulture)) + { + try + { + return (Grammar)assembly.CreateInstance(typeTarget.FullName, false, BindingFlags.CreateInstance, null, onInitParameters, null, null); + } + catch (MissingMemberException) + { + throw new ArgumentException(SR.Get(SRID.RuleScriptInvalidParameters, typeTarget.Name, typeTarget.Name)); + } + } + } + } + } + return null; + } + + #endregion + + #region public Properties + + // Standard properties to control grammar: + + // Controls whether this grammar is actually included in the recognition. True by default. Can be set at any point. + public bool Enabled + { + get { return _enabled; } + set + { + // Note: you can still set or get this property regardless of whether the Grammar is loaded or not. + // In theory we could throw in certain scenarios but this is probably simplest. + if (_grammarState != GrammarState.Unloaded && _enabled != value) + { + _recognizer.SetGrammarState(this, value); + } + _enabled = value; // Only on success + } + } + + // Relative weight of this Grammar/Rule. + public float Weight + { + get { return _weight; } + set + { + if (value < 0.0 || value > 1.0) + { + throw new ArgumentOutOfRangeException(nameof(value), SR.Get(SRID.GrammarInvalidWeight)); + } + // Note: you can still set or get this property regardless of whether the Grammar is loaded or not. + // In theory we could throw in certain scenarios but this is probably simplest. + if (_grammarState != GrammarState.Unloaded && !_weight.Equals(value)) + { + _recognizer.SetGrammarWeight(this, value); + } + _weight = value; // Only on success + } + } + + // Priority of this Grammar/Rule. + // If different grammars have paths which match the same words, + // then the result will be returned for the grammar with the highest priority. + // Default value zero {lowest value}. + public int Priority + { + get { return _priority; } + set + { + if (value < -128 || value > 127) + { + // We could have used sbyte in the signature of this property but int is probably simpler. + throw new ArgumentOutOfRangeException(nameof(value), SR.Get(SRID.GrammarInvalidPriority)); + } + if (_grammarState != GrammarState.Unloaded && _priority != value) + { + _recognizer.SetGrammarPriority(this, value); + } + _priority = value; // Only on success. + } + } + + // Simple property that allows a name to be attached to the Grammar. + // This has no effect but could be convenient for certain apps. + public string Name + { + get { return _grammarName; } + set + { +#pragma warning disable 6507 +#pragma warning disable 6526 + if (value == null) { value = string.Empty; } + _grammarName = value; +#pragma warning restore 6507 +#pragma warning restore 6526 + } + } + public string RuleName + { + get { return _ruleName; } + } + public bool Loaded + { + get { return _grammarState == GrammarState.Loaded; } + } + internal Uri Uri + { + get { return _uri; } + } + + #endregion + + #region public Events + + // The event fired upon a recognition. + public event EventHandler SpeechRecognized; + + #endregion + + #region Internal Properties + + internal IRecognizerInternal Recognizer + { + get { return _recognizer; } + set { _recognizer = value; } + } + + // The load-state of the grammar. + // - Set to New by constructor and also kept as New if a synchronous load fails. + // - Set to Loaded when any grammar load completes. + // - Set to Unloaded when a grammar is unloaded from the Recognizer. + // There are two additional states used for async grammar loading: + // - Set to Loading when an Async load is in progress. + // - Set to LoadFailed when an async load fails but the grammar is still in the Grammars collection. + internal GrammarState State + { + get { return _grammarState; } + set + { + Debug.Assert(value >= GrammarState.Unloaded && value <= GrammarState.LoadFailed); + + // Check state diagram for State. Possible paths: + // Unloaded -> Loaded -> Unloaded {LoadGrammar succeeded}. + // Unloaded {LoadGrammar failed}. + // Unloaded -> Loading -> Loaded -> Unloaded {LoadGrammarAsync succeeded}. + // Unloaded -> Loading -> Unloaded {LoadGrammarAsync cancelled}. + // Unloaded -> Loading -> LoadFailed -> Unloaded {LoadGrammarAsync failed}. + Debug.Assert((_grammarState == GrammarState.Unloaded && (value == GrammarState.Unloaded || value == GrammarState.Loading || value == GrammarState.Loaded)) || + (_grammarState == GrammarState.Loading && (value == GrammarState.LoadFailed || value == GrammarState.Loaded || value == GrammarState.Unloaded)) || + (_grammarState == GrammarState.Loaded && value == GrammarState.Unloaded) || + (_grammarState == GrammarState.LoadFailed && value == GrammarState.Unloaded) + ); + + // If we are unloaded also reset these parameters. + if (value == GrammarState.Unloaded) + { + // Remove references to these objects so they can be garbage collected. + _loadException = null; + _recognizer = null; + + // Don't reset _uri and _ruleName - allows re-use. + // Don't reset _internalData - leave this to the recognizer. + + // Note: After a Grammar is unloaded you can still get and set the Weight, Enabled etc. + } + else if (value == GrammarState.Loaded || value == GrammarState.LoadFailed) + { + Debug.Assert(_recognizer != null); // Must be set before changing state. + + // Don't update any properties - the recognizer owns pulling this data from the Grammar. + } + + _grammarState = value; // On success + } + } + + internal Exception LoadException + { + get { return _loadException; } + set { _loadException = value; } + } + + // There properties are read-only: + + internal byte[] CfgData + { + get { return _cfgData; } + } + + internal Uri BaseUri + { + get { return _baseUri; } + } + + internal bool Sapi53Only + { + get { return _sapi53Only; } + } + + internal uint SapiGrammarId + { + get { return _sapiGrammarId; } + set { _sapiGrammarId = value; } + } + + /// + /// Is the grammar a strongly typed grammar? + /// + protected internal virtual bool IsStg + { + get { return _isStg; } + } + + /// + /// Is the grammar built from an srgs document? + /// + internal bool IsSrgsDocument + { + get { return _isSrgsDocument; } + } + + // Arbitrary data that is attached and removed by the RecognizerBase. + // This allow RecognizerBase.Grammars to be a simple list without the extra data being stored separately. + internal InternalGrammarData InternalData + { + get { return _internalData; } + set { _internalData = value; } + } + + #endregion + + #region Internal Methods + + /// + /// Called by the grammar resource loader to load ruleref. Ruleref have a name, a rule name et eventually + /// parameters. + /// + /// The grammar name can be either pointing to a CFG, an Srgs or DLL (stand alone or GAC). + /// + internal static Grammar Create(string grammarName, string ruleName, string onInitParameter, out Uri redirectUri) + { + redirectUri = null; + + // Look for tell-tell sign that it is an assembly + grammarName = grammarName.Trim(); + + // Get an Uri for the grammar. Could fail for GACed values. + Uri uriGrammar; + bool hasUri = Uri.TryCreate(grammarName, UriKind.Absolute, out uriGrammar); + + int posDll = grammarName.IndexOf(".dll", StringComparison.OrdinalIgnoreCase); + if (!hasUri || (posDll > 0 && posDll == grammarName.Length - 4)) + { + Assembly assembly; + if (hasUri) + { + // regular dll, should use LoadFrom () + if (uriGrammar.IsFile) + { + assembly = Assembly.LoadFrom(uriGrammar.LocalPath); + } + else + { + throw new InvalidOperationException(); + } + } + else + { + // Dll in the GAC use Load () + assembly = Assembly.Load(grammarName); + } + return LoadGrammarFromAssembly(assembly, ruleName, onInitParameter); + } + + try + { + // Standard Srgs or CFG, just create the grammar + string localPath; + using (Stream stream = s_resourceLoader.LoadFile(uriGrammar, out localPath, out redirectUri)) + { + try + { + return new Grammar(onInitParameter, stream, ruleName); + } + finally + { + s_resourceLoader.UnloadFile(localPath); + } + } + } + catch + { + // It was not a CFG or an Srgs, try again as dll + Assembly assembly = Assembly.LoadFrom(grammarName); + return LoadGrammarFromAssembly(assembly, ruleName, onInitParameter); + } + } + + // Method called from the recognizer when a recognition has occurred. + // Only called for SpeechRecognition events, not SpeechRecognitionRejected. + internal void OnRecognitionInternal(SpeechRecognizedEventArgs eventArgs) + { + Debug.Assert(eventArgs.Result.Grammar == this); + + EventHandler recognitionHandler = SpeechRecognized; + if (recognitionHandler != null) + { + recognitionHandler(this, eventArgs); + } + } + + // Helper method used to indicate if this grammar has a dictation Uri or not. + // This is here because the functionality needs to be a common place. + internal static bool IsDictationGrammar(Uri uri) + { + // Note that must check IsAbsoluteUri before Scheme because Uri.Scheme may throw on a relative Uri + if (uri == null || !uri.IsAbsoluteUri || uri.Scheme != "grammar" || + !string.IsNullOrEmpty(uri.Host) || !string.IsNullOrEmpty(uri.Authority) || + !string.IsNullOrEmpty(uri.Query) || uri.PathAndQuery != "dictation") + { + return false; + } + return true; + } + + // Helper method used to indicate if this grammar has a dictation Uri or not. + // This is here because the functionality needs to be a common place. + internal bool IsDictation(Uri uri) + { + bool isDictationGrammar = IsDictationGrammar(uri); + + // Note that must check IsAbsoluteUri before Scheme because Uri.Scheme may throw on a relative Uri + if (!isDictationGrammar && this is DictationGrammar) + { + throw new ArgumentException(SR.Get(SRID.DictationInvalidTopic), nameof(uri)); + } + return isDictationGrammar; + } + + /// + /// Find a grammar in a tree or rule refs grammar from the SAPI grammar Id + /// + /// SAPI id + /// null if not found + internal Grammar Find(long grammarId) + { + if (_ruleRefs != null) + { + foreach (Grammar ruleRef in _ruleRefs) + { + Grammar found; + + if (grammarId == ruleRef._sapiGrammarId) + { + return ruleRef; + } + if ((found = ruleRef.Find(grammarId)) != null) + { + return found; + } + } + } + return null; + } + + /// + /// Find a grammar in a tree or rule refs grammar from a rule name + /// + /// null if not found + internal Grammar Find(string ruleName) + { + if (_ruleRefs != null) + { + foreach (Grammar ruleRef in _ruleRefs) + { + Grammar found; + + if (ruleName == ruleRef.RuleName) + { + return ruleRef; + } + if ((found = ruleRef.Find(ruleName)) != null) + { + return found; + } + } + } + return null; + } + + /// + /// Add a rule ref grammar to a grammar. + /// + internal void AddRuleRef(Grammar ruleRef, uint grammarId) + { + if (_ruleRefs == null) + { + _ruleRefs = new Collection(); + } + _ruleRefs.Add(ruleRef); + _sapiGrammarId = grammarId; + } + + internal MethodInfo MethodInfo(string method) + { + return GetType().GetMethod(method, BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic); + } + + #endregion + + #region Internal Fields + + internal GrammarOptions _semanticTag; + + internal System.Speech.Internal.SrgsCompiler.AppDomainGrammarProxy _proxy; + + internal ScriptRef[] _scripts; + + #endregion + + #region Protected Methods + protected string ResourceName + { + get + { + return _resources; + } + set + { + Helpers.ThrowIfEmptyOrNull(value, nameof(value)); + _resources = value; + } + } + + #endregion + + #region Private Methods + + // Called to initialize the grammar from the passed in data. + // In SpeechFX this is called at construction time. + // In MSS this is {currently} called when GetCfg is called. + // The cfg data is stored in the _cfgData field, which is not currently reset to null ever. + // After calling this method the passed in Stream / SrgsDocument are set to null. + private void LoadAndCompileCfgData(bool isImportedGrammar, bool stgInit) + { +#if DEBUG + Debug.Assert(!_loaded); + _loaded = true; +#endif + + // If strongly typed grammar, load the cfg from the resources otherwise load the IL from within the CFG + Stream stream = IsStg ? LoadCfgFromResource(stgInit) : LoadCfg(isImportedGrammar, stgInit); + + // Check if the grammar needs to be rebuilt + SrgsRule[] extraRules = RunOnInit(IsStg); // list of extra rule to append to the current CFG + if (extraRules != null) + { + MemoryStream streamCombined = CombineCfg(_ruleName, stream, extraRules); + + // Release the old stream since a new one contains the CFG + stream.Close(); + stream = streamCombined; + } + // Note LoadCfg, LoadCfgFromResource and CombineCfg all reset Stream position to zero. + + _cfgData = Helpers.ReadStreamToByteArray(stream, (int)stream.Length); + stream.Close(); + + // Reset these - no longer needed + _srgsDocument = null; + _appStream = null; + } + + /// + /// Returns a stream object for a grammar. + /// + private MemoryStream LoadCfg(bool isImportedGrammar, bool stgInit) + { + // No parameters to the constructors + Uri uriGrammar = Uri; + MemoryStream stream = new(); + + if (uriGrammar != null) + { + throw new PlatformNotSupportedException(); + } + else if (_srgsDocument != null) + { + // If srgs, compile to a stream + SrgsGrammarCompiler.Compile(_srgsDocument, stream); + if (_baseUri == null && _srgsDocument.BaseUri != null) + { + // If we loaded the SrgsDocument from a file then that should be used as the base path. + // But it should not override any baseUri supplied directly to the Grammar constructor or in the xmlBase attribute in the xml. + _baseUri = _srgsDocument.BaseUri; + + // So the priority order for getting the base path is: + // 1. The xml:base attribute in the xml. + // 2. The baseUri passed to the Grammar constructor. + // 3. The path the xml was originally loaded from. + } + } + else if (_grammarBuilder != null) + { + // If GrammarBuilder, compile to a stream + _grammarBuilder.Compile(stream); + } + else + { + // If stream, load + SrgsGrammarCompiler.CompileXmlOrCopyCfg(_appStream, stream, null); + } + + stream.Position = 0; + + // Update the rule name + _ruleName = CheckRuleName(stream, _ruleName, isImportedGrammar, stgInit, out _sapi53Only, out _semanticTag); + + // Create an app domain for the grammar code if any + CreateSandbox(stream); + + stream.Position = 0; + return stream; + } + + /// + /// Look for a grammar by rule name in a loaded assembly. + /// + /// The search goes over the base type for the grammar "rule name" and all of its derived language + /// dependent classes. + /// The matching algorithm pick a class that match the culture. + /// + private static Grammar LoadGrammarFromAssembly(Assembly assembly, string ruleName, string onInitParameters) + { + Type grammarType = typeof(Grammar); + Type matchingType = null; + + foreach (Type typeTarget in assembly.GetTypes()) + { + // must be a grammar object + if (typeTarget.IsSubclassOf(grammarType)) + { + string cultureId = null; + + // Set the base class for this rule + if (typeTarget.Name == ruleName) + { + matchingType = typeTarget; + } + + // Pick a class that derives from rulename + if (typeTarget == matchingType || (matchingType != null && typeTarget.IsSubclassOf(matchingType))) + { + // Check if the language match + if (typeTarget.GetField("__cultureId") != null) + { + // Get the association table + try + { + cultureId = (string)typeTarget.InvokeMember("__cultureId", BindingFlags.GetField, null, null, null, null); + } + catch (Exception e) + { + if (!(e is System.MissingFieldException)) + { + throw; + } + } + + // Check for the current culture or any compatible culture (parent en-us or en for e.g.) + if (Helpers.CompareInvariantCulture(new CultureInfo(int.Parse(cultureId, CultureInfo.InvariantCulture)), CultureInfo.CurrentUICulture)) + { + try + { + object[] initParams = MatchInitParameters(typeTarget, onInitParameters, assembly.GetName().Name, ruleName); + + // The CLR does the match for the right constructor based on the onInitParameters types + return (Grammar)assembly.CreateInstance(typeTarget.FullName, false, BindingFlags.CreateInstance, null, initParams, null, null); + } + catch (MissingMemberException) + { + throw new ArgumentException(SR.Get(SRID.RuleScriptInvalidParameters, typeTarget.Name, typeTarget.Name)); + } + } + } + } + } + } + return null; + } + + /// + /// Construct a list of parameters from a sapi:params string. + /// + private static object[] MatchInitParameters(Type type, string onInitParameters, string grammar, string rule) + { + ConstructorInfo[] cis = type.GetConstructors(); + NameValuePair[] pairs = ParseInitParams(onInitParameters); + object[] values = new object[pairs.Length]; + bool foundConstructor = false; + for (int iCtor = 0; iCtor < cis.Length && !foundConstructor; iCtor++) + { + ParameterInfo[] paramInfo = cis[iCtor].GetParameters(); + + // Check if enough parameters are provided. + if (paramInfo.Length > pairs.Length) + { + continue; + } + foundConstructor = true; + for (int i = 0; i < pairs.Length && foundConstructor; i++) + { + NameValuePair pair = pairs[i]; + + // anonymous + if (pair._name == null) + { + values[i] = pair._value; + } + else + { + bool foundParameter = false; + for (int j = 0; j < paramInfo.Length; j++) + { + if (paramInfo[j].Name == pair._name) + { + values[j] = ParseValue(paramInfo[j].ParameterType, pair._value); + foundParameter = true; + break; + } + } + if (!foundParameter) + { + foundConstructor = false; + } + } + } + } + if (!foundConstructor) + { + throw new FormatException(SR.Get(SRID.CantFindAConstructor, grammar, rule, FormatConstructorParameters(cis))); + } + return values; + } + + /// + /// Parse the value for a type from a string to a strong type. + /// If the type does not support the Parse method then the operation fails. + /// + private static object ParseValue(Type type, string value) + { + if (type == typeof(string)) + { + return value; + } + return type.InvokeMember("Parse", BindingFlags.InvokeMethod, null, null, new object[] { value }, CultureInfo.InvariantCulture); + } + + /// + /// Returns the list of the possible parameter names and type for a grammar + /// + private static string FormatConstructorParameters(ConstructorInfo[] cis) + { + StringBuilder sb = new(); + for (int iCtor = 0; iCtor < cis.Length; iCtor++) + { + sb.Append(iCtor > 0 ? " or sapi:parms=\"" : "sapi:parms=\""); + ParameterInfo[] pis = cis[iCtor].GetParameters(); + for (int i = 0; i < pis.Length; i++) + { + if (i > 0) + { + sb.Append(';'); + } + ParameterInfo pi = pis[i]; + sb.Append(pi.Name); + sb.Append(':'); + sb.Append(pi.ParameterType.Name); + } + sb.Append('"'); + } + return sb.ToString(); + } + + /// + /// Split the init parameter strings into an array of name/values + /// The format must be "name:value". If the ':' then parameter is anonymous. + /// + private static NameValuePair[] ParseInitParams(string initParameters) + { + if (string.IsNullOrEmpty(initParameters)) + { + return Array.Empty(); ; + } + + string[] parameters = initParameters.Split(new char[] { ';' }, StringSplitOptions.None); + NameValuePair[] pairs = new NameValuePair[parameters.Length]; + + for (int i = 0; i < parameters.Length; i++) + { + string parameter = parameters[i]; + int posColon = parameter.IndexOf(':'); + if (posColon >= 0) + { + pairs[i]._name = parameter.Substring(0, posColon); + pairs[i]._value = parameter.Substring(posColon + 1); + } + else + { + pairs[i]._value = parameter; + } + } + return pairs; + } + + private void InitialGrammarLoad(string ruleName, object[] parameters, bool isImportedGrammar) + { + _ruleName = ruleName; + _parameters = parameters; + + // Bail out if it is a dictation grammar + if (!IsDictation(_uri)) + { + LoadAndCompileCfgData(isImportedGrammar, false); + } + } + + private void CreateSandbox(MemoryStream stream) + { + // Checks if it contains .NET Semantic code + byte[] assemblyContent; + byte[] assemblyDebugSymbols; + ScriptRef[] scripts; + stream.Position = 0; + + // This must be before the SAPI load to avoid some conflict with SAPI server when getting at the + // the stream + if (System.Speech.Internal.SrgsCompiler.CfgGrammar.LoadIL(stream, out assemblyContent, out assemblyDebugSymbols, out scripts)) + { + // Check all methods referenced in the rule; availability, public and arguments + Assembly executingAssembly = Assembly.GetExecutingAssembly(); + _proxy = new AppDomainGrammarProxy(); + _proxy.Init(_ruleName, assemblyContent, assemblyDebugSymbols); + _scripts = scripts; + } + } + + // Loads a strongly typed grammar from a resource in the Assembly. + private Stream LoadCfgFromResource(bool stgInit) + { + // Strongly typed grammar get the Cfg data + Assembly assembly = Assembly.GetAssembly(GetType()); + + Stream stream = assembly.GetManifestResourceStream(ResourceName); + + if (stream == null) + { + throw new FormatException(SR.Get(SRID.RecognizerInvalidBinaryGrammar)); + } + try + { + ScriptRef[] scripts = CfgGrammar.LoadIL(stream); + if (scripts == null) + { + throw new ArgumentException(SR.Get(SRID.CannotLoadDotNetSemanticCode)); + } + _scripts = scripts; + } + catch (Exception e) + { + throw new ArgumentException(SR.Get(SRID.CannotLoadDotNetSemanticCode), e); + } + stream.Position = 0; + + // Update the rule name + _ruleName = CheckRuleName(stream, GetType().Name, false, stgInit, out _sapi53Only, out _semanticTag); + + _isStg = true; + return stream; + } + + private static MemoryStream CombineCfg(string rule, Stream stream, SrgsRule[] extraRules) + { + using (MemoryStream streamExtra = new()) + { + // Create an SrgsDocument from the set of rules + SrgsDocument sgrsDocument = new(); + sgrsDocument.TagFormat = SrgsTagFormat.KeyValuePairs; + foreach (SrgsRule srgsRule in extraRules) + { + sgrsDocument.Rules.Add(srgsRule); + } + + SrgsGrammarCompiler.Compile(sgrsDocument, streamExtra); + + using (StreamMarshaler streamMarshaler = new(stream)) + { + long endSeekPosition = stream.Position; + Backend backend = new(streamMarshaler); + stream.Position = endSeekPosition; + + streamExtra.Position = 0; + MemoryStream streamCombined = new(); + using (StreamMarshaler streamExtraMarshaler = new(streamExtra)) + { + Backend extra = new(streamExtraMarshaler); + Backend combined = Backend.CombineGrammar(rule, backend, extra); + + using (StreamMarshaler streamCombinedMarshaler = new(streamCombined)) + { + combined.Commit(streamCombinedMarshaler); + streamCombined.Position = 0; + return streamCombined; + } + } + } + } + } + +#pragma warning disable 56507 // check for null or empty strings + + private SrgsRule[] RunOnInit(bool stg) + { + SrgsRule[] extraRules = null; + bool onInitInvoked = false; + + // Get the name of the onInit method to run + string methodName = ScriptRef.OnInitMethod(_scripts, _ruleName); + + if (methodName != null) + { + if (_proxy != null) + { + Exception appDomainException; + extraRules = _proxy.OnInit(methodName, _parameters, _onInitParameters, out appDomainException); + onInitInvoked = true; + if (appDomainException != null) + { + ExceptionDispatchInfo.Throw(appDomainException); + } + } + else + { + // call OnInit if any - should be based on Rule + Type[] types = new Type[_parameters.Length]; + + for (int i = 0; i < _parameters.Length; i++) + { + types[i] = _parameters[i].GetType(); + } + MethodInfo onInit = GetType().GetMethod(methodName, types); + + // If somehow we failed to find a constructor, let the system handle it + if (onInit != null) + { + System.Diagnostics.Debug.Assert(_parameters != null); + extraRules = (SrgsRule[])onInit.Invoke(this, _parameters); + onInitInvoked = true; + } + else + { + throw new ArgumentException(SR.Get(SRID.RuleScriptInvalidParameters, _ruleName, _ruleName)); + } + } + } + + // Cannot have onInit parameters if onInit has not been invoked. + if (!stg && !onInitInvoked && _parameters != null) + { + throw new ArgumentException(SR.Get(SRID.RuleScriptInvalidParameters, _ruleName, _ruleName)); + } + return extraRules; + } + + // Pulls the required data out of a stream containing a cfg. + // Stream must point to start of cfg on entry and is reset to same point on exit. + private static string CheckRuleName(Stream stream, string rulename, bool isImportedGrammar, bool stgInit, out bool sapi53Only, out GrammarOptions grammarOptions) + { + sapi53Only = false; + long initialPosition = stream.Position; + + CfgGrammar.CfgHeader header; + using (StreamMarshaler streamHelper = new(stream)) // Use StreamMarshaler which helps deserialize certain data types + { + CfgGrammar.CfgSerializedHeader serializedHeader = null; + header = CfgGrammar.ConvertCfgHeader(streamHelper, false, true, out serializedHeader); + + StringBlob symbols = header.pszSymbols; + + // Calc the root rule + string rootRule = header.ulRootRuleIndex != 0xffffffff && header.ulRootRuleIndex < header.rules.Length ? symbols.FromOffset(header.rules[header.ulRootRuleIndex]._nameOffset) : null; + + // Get if we have semantic interpretation + sapi53Only = (header.GrammarOptions & (GrammarOptions.MssV1 | GrammarOptions.W3cV1 | GrammarOptions.STG | GrammarOptions.IpaPhoneme)) != 0; + + // Check that the rule name is valid + if (rootRule == null && string.IsNullOrEmpty(rulename)) + { + throw new ArgumentException(SR.Get(SRID.SapiErrorNoRulesToActivate)); + } + + if (!string.IsNullOrEmpty(rulename)) + { + // Convert the CFG script reference to ScriptRef + bool fFoundRule = false; + foreach (CfgRule cfgRule in header.rules) + { + if (symbols.FromOffset(cfgRule._nameOffset) == rulename) + { + // Private rule are not allowed + fFoundRule = cfgRule.Export || stgInit || (!isImportedGrammar ? cfgRule.TopLevel || rulename == rootRule : false); + break; + } + } + + // check that the name exists + if (!fFoundRule) + { + throw new ArgumentException(SR.Get(SRID.RecognizerRuleNotFoundStream, rulename)); + } + } + else + { + rulename = rootRule; + } + + grammarOptions = header.GrammarOptions & GrammarOptions.TagFormat; + } + stream.Position = initialPosition; + return rulename; + } + + #endregion + + #region Private Fields + +#pragma warning disable 56524 // You cannot dispose an object we don't create + + private byte[] _cfgData; + + private Stream _appStream; + private bool _isSrgsDocument; + private SrgsDocument _srgsDocument; + + private GrammarBuilder _grammarBuilder; + +#pragma warning restore 56524 + + private IRecognizerInternal _recognizer; + private GrammarState _grammarState; + private Exception _loadException; + private Uri _uri; + private Uri _baseUri; + private string _ruleName; + private string _resources; + private object[] _parameters; + private string _onInitParameters; + private bool _enabled = true; + private bool _isStg; + private bool _sapi53Only; + private uint _sapiGrammarId; + private float _weight = 1.0f; + private int _priority; + private InternalGrammarData _internalData; + private string _grammarName = string.Empty; + private Collection _ruleRefs; + private static ResourceLoader s_resourceLoader = new(); + +#if DEBUG + private bool _loaded; +#endif + + #endregion + + #region Private Types + + private struct NameValuePair + { + internal string _name; + internal string _value; + } + + #endregion + } + + // Grammar load-state. Not public. + internal enum GrammarState + { + Unloaded, + Loading, + Loaded, + LoadFailed, + } +} diff --git a/src/libraries/System.Speech/src/Recognition/GrammarBuilder.cs b/src/libraries/System.Speech/src/Recognition/GrammarBuilder.cs new file mode 100644 index 00000000000000..c08faeafd2dbf9 --- /dev/null +++ b/src/libraries/System.Speech/src/Recognition/GrammarBuilder.cs @@ -0,0 +1,534 @@ +// 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.Diagnostics; +using System.Globalization; +using System.IO; +using System.Speech.Internal; +using System.Speech.Internal.GrammarBuilding; +using System.Speech.Internal.SrgsCompiler; +using System.Speech.Internal.SrgsParser; +using System.Text; + +namespace System.Speech.Recognition +{ + [DebuggerDisplay("{DebugSummary}")] + public class GrammarBuilder + { + #region Constructors + + public GrammarBuilder() + { + _grammarBuilder = new InternalGrammarBuilder(); + } + + public GrammarBuilder(string phrase) + : this() + { + Append(phrase); + } + + public GrammarBuilder(string phrase, SubsetMatchingMode subsetMatchingCriteria) + : this() + { + Append(phrase, subsetMatchingCriteria); + } + + public GrammarBuilder(string phrase, int minRepeat, int maxRepeat) + : this() + { + Append(phrase, minRepeat, maxRepeat); + } + + public GrammarBuilder(GrammarBuilder builder, int minRepeat, int maxRepeat) + : this() + { + Append(builder, minRepeat, maxRepeat); + } + + public GrammarBuilder(Choices alternateChoices) + : this() + { + Append(alternateChoices); + } + + public GrammarBuilder(SemanticResultKey key) + : this() + { + Append(key); + } + + public GrammarBuilder(SemanticResultValue value) + : this() + { + Append(value); + } + + #endregion Constructors + + #region Public Methods + + // Append connecting words + + public void Append(string phrase) + { + Helpers.ThrowIfEmptyOrNull(phrase, nameof(phrase)); + + AddItem(new GrammarBuilderPhrase(phrase)); + } + + public void Append(string phrase, SubsetMatchingMode subsetMatchingCriteria) + { + Helpers.ThrowIfEmptyOrNull(phrase, nameof(phrase)); + GrammarBuilder.ValidateSubsetMatchingCriteriaArgument(subsetMatchingCriteria, nameof(subsetMatchingCriteria)); + + AddItem(new GrammarBuilderPhrase(phrase, subsetMatchingCriteria)); + } + + public void Append(string phrase, int minRepeat, int maxRepeat) + { + Helpers.ThrowIfEmptyOrNull(phrase, nameof(phrase)); + GrammarBuilder.ValidateRepeatArguments(minRepeat, maxRepeat, "minRepeat", "maxRepeat"); + + // Wrap the phrase in an item if min and max repeat are set + GrammarBuilderPhrase elementPhrase = new(phrase); + if (minRepeat != 1 || maxRepeat != 1) + { + AddItem(new ItemElement(elementPhrase, minRepeat, maxRepeat)); + } + else + { + AddItem(elementPhrase); + } + } + + // Append list of rulerefs + + public void Append(GrammarBuilder builder) + { + Helpers.ThrowIfNull(builder, nameof(builder)); + + // Should never happens has it is a RO value + Helpers.ThrowIfNull(builder.InternalBuilder, "builder.InternalBuilder"); + Helpers.ThrowIfNull(builder.InternalBuilder.Items, "builder.InternalBuilder.Items"); + + // Clone the items if we are playing with the local list. + foreach (GrammarBuilderBase item in builder.InternalBuilder.Items) + { + if (item == null) + { + // This should never happen! + throw new ArgumentException(SR.Get(SRID.ArrayOfNullIllegal), nameof(builder)); + } + } + + // Clone the items if we are playing with the local list. + List items = builder == this ? builder.Clone().InternalBuilder.Items : builder.InternalBuilder.Items; + + foreach (GrammarBuilderBase item in items) + { + AddItem(item); + } + } + + // Append one-of + + public void Append(Choices alternateChoices) + { + Helpers.ThrowIfNull(alternateChoices, nameof(alternateChoices)); + + AddItem(alternateChoices.OneOf); + } + + public void Append(SemanticResultKey key) + { + Helpers.ThrowIfNull(key, "builder"); + + AddItem(key.SemanticKeyElement); + } + + public void Append(SemanticResultValue value) + { + Helpers.ThrowIfNull(value, "builder"); + + AddItem(value.Tag); + } + + public void Append(GrammarBuilder builder, int minRepeat, int maxRepeat) + { + Helpers.ThrowIfNull(builder, nameof(builder)); + GrammarBuilder.ValidateRepeatArguments(minRepeat, maxRepeat, "minRepeat", "maxRepeat"); + + // Should never happens has it is a RO value + Helpers.ThrowIfNull(builder.InternalBuilder, "builder.InternalBuilder"); + + // Wrap the phrase in an item if min and max repeat are set + if (minRepeat != 1 || maxRepeat != 1) + { + AddItem(new ItemElement(builder.InternalBuilder.Items, minRepeat, maxRepeat)); + } + else + { + Append(builder); + } + } + + // Append dictation element + + public void AppendDictation() + { + AddItem(new GrammarBuilderDictation()); + } + + public void AppendDictation(string category) + { + Helpers.ThrowIfEmptyOrNull(category, nameof(category)); + + AddItem(new GrammarBuilderDictation(category)); + } + + // Append wildcard element + + public void AppendWildcard() + { + AddItem(new GrammarBuilderWildcard()); + } + + /// + /// Append external rule ref + /// + public void AppendRuleReference(string path) + { + Helpers.ThrowIfEmptyOrNull(path, nameof(path)); + Uri uri; + + try + { + uri = new Uri(path, UriKind.RelativeOrAbsolute); + } + catch (UriFormatException e) + { + throw new ArgumentException(e.Message, path, e); + } + + AddItem(new GrammarBuilderRuleRef(uri, null)); + } + + /// + /// Append external rule ref + /// + public void AppendRuleReference(string path, string rule) + { + Helpers.ThrowIfEmptyOrNull(path, nameof(path)); + Helpers.ThrowIfEmptyOrNull(rule, nameof(rule)); + Uri uri; + + try + { + uri = new Uri(path, UriKind.RelativeOrAbsolute); + } + catch (UriFormatException e) + { + throw new ArgumentException(e.Message, path, e); + } + + AddItem(new GrammarBuilderRuleRef(uri, rule)); + } + public string DebugShowPhrases + { + get + { + return DebugSummary; + } + } + + #endregion Constructors + + #region Public Properties + public CultureInfo Culture + { + get + { + return _culture; + } + set + { + if (value == null) + { + throw new ArgumentNullException(nameof(value)); + } + + _culture = value; + } + } + + #endregion + + #region Operator Overloads + + public static GrammarBuilder operator +(string phrase, GrammarBuilder builder) + { + return Add(phrase, builder); + } + + public static GrammarBuilder Add(string phrase, GrammarBuilder builder) + { + Helpers.ThrowIfNull(builder, nameof(builder)); + + GrammarBuilder grammar = new(phrase); + grammar.Append(builder); + return grammar; + } + + public static GrammarBuilder operator +(GrammarBuilder builder, string phrase) + { + return Add(builder, phrase); + } + + public static GrammarBuilder Add(GrammarBuilder builder, string phrase) + { + Helpers.ThrowIfNull(builder, nameof(builder)); + + GrammarBuilder grammar = builder.Clone(); + grammar.Append(phrase); + return grammar; + } + + public static GrammarBuilder operator +(Choices choices, GrammarBuilder builder) + { + return Add(choices, builder); + } + + public static GrammarBuilder Add(Choices choices, GrammarBuilder builder) + { + Helpers.ThrowIfNull(choices, nameof(choices)); + Helpers.ThrowIfNull(builder, nameof(builder)); + + GrammarBuilder grammar = new(choices); + grammar.Append(builder); + return grammar; + } + + public static GrammarBuilder operator +(GrammarBuilder builder, Choices choices) + { + return Add(builder, choices); + } + + public static GrammarBuilder Add(GrammarBuilder builder, Choices choices) + { + Helpers.ThrowIfNull(builder, nameof(builder)); + Helpers.ThrowIfNull(choices, nameof(choices)); + + GrammarBuilder grammar = builder.Clone(); + grammar.Append(choices); + return grammar; + } + + public static GrammarBuilder operator +(GrammarBuilder builder1, GrammarBuilder builder2) + { + return Add(builder1, builder2); + } + + public static GrammarBuilder Add(GrammarBuilder builder1, GrammarBuilder builder2) + { + Helpers.ThrowIfNull(builder1, nameof(builder1)); + Helpers.ThrowIfNull(builder2, nameof(builder2)); + + GrammarBuilder grammar = builder1.Clone(); + grammar.Append(builder2); + return grammar; + } + public static implicit operator GrammarBuilder(string phrase) { return new GrammarBuilder(phrase); } + public static implicit operator GrammarBuilder(Choices choices) { return new GrammarBuilder(choices); } + public static implicit operator GrammarBuilder(SemanticResultKey semanticKey) { return new GrammarBuilder(semanticKey); } + public static implicit operator GrammarBuilder(SemanticResultValue semanticValue) { return new GrammarBuilder(semanticValue); } + + #endregion + + #region Internal Methods + + internal static void ValidateRepeatArguments(int minRepeat, int maxRepeat, string minParamName, string maxParamName) + { + if (minRepeat < 0) + { + throw new ArgumentOutOfRangeException(minParamName, SR.Get(SRID.InvalidMinRepeat, minRepeat)); + } + if (minRepeat > maxRepeat) + { + throw new ArgumentException(SR.Get(SRID.MinGreaterThanMax), maxParamName); + } + } + + internal static void ValidateSubsetMatchingCriteriaArgument(SubsetMatchingMode subsetMatchingCriteria, string paramName) + { + switch (subsetMatchingCriteria) + { + case SubsetMatchingMode.OrderedSubset: + case SubsetMatchingMode.OrderedSubsetContentRequired: + case SubsetMatchingMode.Subsequence: + case SubsetMatchingMode.SubsequenceContentRequired: + break; + default: + throw new ArgumentException(SR.Get(SRID.EnumInvalid, paramName), paramName); + } + } + + internal void CreateGrammar(IElementFactory elementFactory) + { + // Create a new Identifier Collection which will provide unique ids + // for each rule + IdentifierCollection ruleIds = new(); + elementFactory.Grammar.Culture = Culture; + + _grammarBuilder.CreateElement(elementFactory, null, null, ruleIds); + } + + internal void Compile(Stream stream) + { + Backend backend = new(); + CustomGrammar cg = new(); + SrgsElementCompilerFactory elementFactory = new(backend, cg); + CreateGrammar(elementFactory); + + // Optimize in-memory graph representation of the grammar. + backend.Optimize(); + + using (StreamMarshaler streamHelper = new(stream)) + { + backend.Commit(streamHelper); + } + + stream.Position = 0; + } + + internal GrammarBuilder Clone() + { + GrammarBuilder builder = new(); + builder._grammarBuilder = (InternalGrammarBuilder)_grammarBuilder.Clone(); + + return builder; + } + + #endregion + + #region Internal Properties + + internal virtual string DebugSummary + { + get + { + StringBuilder sb = new(); + + foreach (GrammarBuilderBase item in InternalBuilder.Items) + { + if (sb.Length > 0) + { + sb.Append(' '); + } + sb.Append(item.DebugSummary); + } + return sb.ToString(); + } + } + + internal BuilderElements InternalBuilder + { + get + { + return _grammarBuilder; + } + } + + #endregion + + #region Private Methods + + private void AddItem(GrammarBuilderBase item) + { + InternalBuilder.Items.Add(item.Clone()); + } + + #endregion + + #region Private Fields + + private InternalGrammarBuilder _grammarBuilder; + + private CultureInfo _culture = CultureInfo.CurrentUICulture; + + #endregion + + #region Private Type + + private class InternalGrammarBuilder : BuilderElements + { + #region Internal Methods + + internal override GrammarBuilderBase Clone() + { + InternalGrammarBuilder newGrammarbuilder = new(); + foreach (GrammarBuilderBase i in Items) + { + newGrammarbuilder.Items.Add(i.Clone()); + } + return newGrammarbuilder; + } + + internal override IElement CreateElement(IElementFactory elementFactory, IElement parent, IRule rule, IdentifierCollection ruleIds) + { + Collection newRules = new(); + CalcCount(null); + Optimize(newRules); + + foreach (GrammarBuilderBase baseRule in newRules) + { + Items.Add(baseRule); + } + + // The id of the root rule + string rootId = ruleIds.CreateNewIdentifier("root"); + + // Set the grammar's root rule + elementFactory.Grammar.Root = rootId; + elementFactory.Grammar.TagFormat = System.Speech.Recognition.SrgsGrammar.SrgsTagFormat.KeyValuePairs; + + // Create the root rule + IRule root = elementFactory.Grammar.CreateRule(rootId, RulePublic.False, RuleDynamic.NotSet, false); + + // Create all the rules + foreach (GrammarBuilderBase item in Items) + { + if (item is RuleElement) + { + item.CreateElement(elementFactory, root, root, ruleIds); + } + } + // Create an item which represents the grammar + foreach (GrammarBuilderBase item in Items) + { + if (!(item is RuleElement)) + { + IElement element = item.CreateElement(elementFactory, root, root, ruleIds); + + if (element != null) + { + element.PostParse(root); + elementFactory.AddElement(root, element); + } + } + } + // Post parse the root rule + root.PostParse(elementFactory.Grammar); + + elementFactory.Grammar.PostParse(null); + return null; + } + + #endregion + } + + #endregion + } +} diff --git a/src/libraries/System.Speech/src/Recognition/IRecognizerInternal.cs b/src/libraries/System.Speech/src/Recognition/IRecognizerInternal.cs new file mode 100644 index 00000000000000..8a9dad4261d692 --- /dev/null +++ b/src/libraries/System.Speech/src/Recognition/IRecognizerInternal.cs @@ -0,0 +1,23 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace System.Speech.Recognition +{ + // Interface that all recognizers must implement in order to connect to Grammar and RecognitionResult. + internal interface IRecognizerInternal + { + #region Internal Methods + + void SetGrammarState(Grammar grammar, bool enabled); + + void SetGrammarWeight(Grammar grammar, float weight); + + void SetGrammarPriority(Grammar grammar, int priority); + + Grammar GetGrammarFromId(ulong id); + + void SetDictationContext(Grammar grammar, string precedingText, string subsequentText); + + #endregion + } +} diff --git a/src/libraries/System.Speech/src/Recognition/LoadGrammarCompletedEventArgs.cs b/src/libraries/System.Speech/src/Recognition/LoadGrammarCompletedEventArgs.cs new file mode 100644 index 00000000000000..1453382aa0be92 --- /dev/null +++ b/src/libraries/System.Speech/src/Recognition/LoadGrammarCompletedEventArgs.cs @@ -0,0 +1,39 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.ComponentModel; + +namespace System.Speech.Recognition +{ + // Event args used in the LoadGrammarCompleted event. + + public class LoadGrammarCompletedEventArgs : AsyncCompletedEventArgs + { + #region Constructors + + internal LoadGrammarCompletedEventArgs(Grammar grammar, Exception error, bool cancelled, object userState) + : base(error, cancelled, userState) + { + _grammar = grammar; + } + + #endregion + + #region Public Properties + public Grammar Grammar + { + get { return _grammar; } + } + + #endregion + + #region Private Fields + +#pragma warning disable 6524 + private Grammar _grammar; +#pragma warning restore 6524 + + #endregion + + } +} diff --git a/src/libraries/System.Speech/src/Recognition/RecognizeCompletedEventArgs.cs b/src/libraries/System.Speech/src/Recognition/RecognizeCompletedEventArgs.cs new file mode 100644 index 00000000000000..bb22a7cc8e213c --- /dev/null +++ b/src/libraries/System.Speech/src/Recognition/RecognizeCompletedEventArgs.cs @@ -0,0 +1,60 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.ComponentModel; + +namespace System.Speech.Recognition +{ + public class RecognizeCompletedEventArgs : AsyncCompletedEventArgs + { + #region Constructors + + internal RecognizeCompletedEventArgs(RecognitionResult result, bool initialSilenceTimeout, bool babbleTimeout, + bool inputStreamEnded, TimeSpan audioPosition, + Exception error, bool cancelled, object userState) + : base(error, cancelled, userState) + { + _result = result; + _initialSilenceTimeout = initialSilenceTimeout; + _babbleTimeout = babbleTimeout; + _inputStreamEnded = inputStreamEnded; + _audioPosition = audioPosition; + } + + #endregion + + #region Public Properties + public RecognitionResult Result + { + get { return _result; } + } + public bool InitialSilenceTimeout + { + get { return _initialSilenceTimeout; } + } + public bool BabbleTimeout + { + get { return _babbleTimeout; } + } + public bool InputStreamEnded + { + get { return _inputStreamEnded; } + } + public TimeSpan AudioPosition + { + get { return _audioPosition; } + } + + #endregion + + #region Private Fields + + private RecognitionResult _result; + private bool _initialSilenceTimeout; + private bool _babbleTimeout; + private bool _inputStreamEnded; + private TimeSpan _audioPosition; + + #endregion + } +} diff --git a/src/libraries/System.Speech/src/Recognition/RecognizeMode.cs b/src/libraries/System.Speech/src/Recognition/RecognizeMode.cs new file mode 100644 index 00000000000000..841e854ba59b48 --- /dev/null +++ b/src/libraries/System.Speech/src/Recognition/RecognizeMode.cs @@ -0,0 +1,11 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace System.Speech.Recognition +{ + public enum RecognizeMode + { + Single, + Multiple + } +} diff --git a/src/libraries/System.Speech/src/Recognition/RecognizerBase.cs b/src/libraries/System.Speech/src/Recognition/RecognizerBase.cs new file mode 100644 index 00000000000000..bc565820121337 --- /dev/null +++ b/src/libraries/System.Speech/src/Recognition/RecognizerBase.cs @@ -0,0 +1,3255 @@ +// 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.Diagnostics; +using System.Globalization; +using System.IO; +using System.Runtime.CompilerServices; +using System.Runtime.ExceptionServices; +using System.Runtime.InteropServices; +using System.Runtime.InteropServices.ComTypes; +using System.Speech.AudioFormat; +using System.Speech.Internal; +using System.Speech.Internal.ObjectTokens; +using System.Speech.Internal.SapiInterop; +using System.Threading; + +namespace System.Speech.Recognition +{ + internal class RecognizerBase : IRecognizerInternal, IDisposable, +ISpGrammarResourceLoader + { + #region Constructors + + public void Dispose() + { + Dispose(true); + GC.SuppressFinalize(this); + } + + ~RecognizerBase() + { + Dispose(false); + } + + #endregion + + #region Internal Methods + + #region Methods to Load and Unload grammars: + + // Synchronous: + internal void LoadGrammar(Grammar grammar) + { + try + { + ValidateGrammar(grammar, GrammarState.Unloaded); + + // Stream and SrgsDocument Grammars get reset on Unload and can't be loaded again. Url Grammars can be reloaded. + if (!_supportsSapi53) + { + CheckGrammarOptionsOnSapi51(grammar); + } + + // Create sapi grammar + // Make the sapi grammar and the id + ulong grammarId; + SapiGrammar sapiGrammar = CreateNewSapiGrammar(out grammarId); + + // Load the data into SAPI: + try + { + LoadSapiGrammar(grammar, sapiGrammar, grammar.Enabled, grammar.Weight, grammar.Priority); + } + catch + { + // Release the SAPI object on error. + sapiGrammar.Dispose(); + + // Set the State to Unloaded. + grammar.State = GrammarState.Unloaded; + grammar.InternalData = null; + + // re-throw exception + throw; + } + + // Create the InternalGrammarData object: + grammar.InternalData = new InternalGrammarData(grammarId, sapiGrammar, grammar.Enabled, grammar.Weight, grammar.Priority); + + // Add to collection: + lock (SapiRecognizer) // Lock to prevent anyone enumerating _grammars from failing + { + _grammars.Add(grammar); + } + + grammar.Recognizer = this; + grammar.State = GrammarState.Loaded; + + // Note on failure in LoadGrammar() the state remains at New and the Grammar is not added to the collection. + // This is in contrast to an asynchronous load where the state is set to LoadFailed and the Grammar is added. + } + catch (Exception e) + { + _loadException = e; + throw; + } + } + + // Asynchronous: + internal void LoadGrammarAsync(Grammar grammar) + { + // Stream and SrgsDocument Grammars get reset on Unload and can't be loaded again. Url Grammars can be reloaded. + if (!_supportsSapi53) + { + CheckGrammarOptionsOnSapi51(grammar); + } + ValidateGrammar(grammar, GrammarState.Unloaded); + + // Various methods like SetGrammarState get simpler if there's a SAPI grammar attached to every Grammar. + // So create sapi grammar and attach to the Internal data before starting the load. + ulong grammarId; + SapiGrammar sapiGrammar = CreateNewSapiGrammar(out grammarId); + + // Make the container for the sapiGrammar and cached property values. + grammar.InternalData = new InternalGrammarData(grammarId, sapiGrammar, grammar.Enabled, grammar.Weight, grammar.Priority); + + // Add to collection: + lock (SapiRecognizer) // Lock to prevent anyone enumerating _grammars from failing + { + _grammars.Add(grammar); + } + + grammar.Recognizer = this; + grammar.State = GrammarState.Loading; + + // Increment the OperationLock to indicate we are loading a grammar asynchronously. + _waitForGrammarsToLoad.StartOperation(); + + // Do the actual load on a thread pool callback. + if (!ThreadPool.QueueUserWorkItem(new WaitCallback(LoadGrammarAsyncCallback), grammar)) + { + throw new OperationCanceledException(SR.Get(SRID.OperationAborted)); + } + } + + // Unload grammars: + internal void UnloadGrammar(Grammar grammar) + { + // Currently we have no good way of deleting grammars that are still being loaded. + ValidateGrammar(grammar, GrammarState.Loaded, GrammarState.LoadFailed); + + // Delete SAPI grammar + InternalGrammarData grammarData = grammar.InternalData; + // Both in the Loaded and LoadFailed state the sapi grammar should still exist. + if (grammarData != null) + { + Debug.Assert(grammarData._sapiGrammar != null); + grammarData._sapiGrammar.Dispose(); + } + + // Remove from collection + lock (SapiRecognizer) // Lock to prevent anyone enumerating _grammars from failing + { + _grammars.Remove(grammar); + } + + // Mark grammar as dead + grammar.State = GrammarState.Unloaded; + grammar.InternalData = null; + } + internal void UnloadAllGrammars() + { + // Use a new collection as otherwise can't delete from current enumeration. + List snapshotGrammars; + lock (SapiRecognizer) + { + snapshotGrammars = new List(_grammars); + } + + // If there is any grammar being loaded asynchronously, wait for the operation to finish first + _waitForGrammarsToLoad.WaitForOperationsToFinish(); + + foreach (Grammar grammar in snapshotGrammars) + { + UnloadGrammar(grammar); + } + + // At the moment there's no way to delete all RecoGrammars in SAPI without individually releasing each one. + // If there was such a mechanism it might be faster than looping through every Grammar. + } + + #endregion + + #region IRecognizerInternal implementation + + void IRecognizerInternal.SetGrammarState(Grammar grammar, bool enabled) + { + Debug.Assert(grammar != null); + Debug.Assert(grammar.Recognizer == this); + + // Note: In all states where Grammar is attached to Recognizer {Loading, Loaded, LoadFailed) + // then the sapiGrammar will be non-null. + + InternalGrammarData grammarData = grammar.InternalData; + Debug.Assert(grammarData != null && grammarData._sapiGrammar != null); + + // Take the lock so things like the changing of the grammar state to Loaded, or the completion of the load + // and call to SetSapiGrammarProperties cannot be happening on the background thread. + lock (_grammarDataLock) + { + // If the grammar is actually loaded then update its state in sapi. + if (grammar.Loaded) + { + grammarData._sapiGrammar.SetGrammarState(enabled ? SPGRAMMARSTATE.SPGS_ENABLED : SPGRAMMARSTATE.SPGS_DISABLED); + } + + // Otherwise just update the local copy so it gets set correctly when Loaded. + grammarData._grammarEnabled = enabled; + } + + // Note - after disabling a Grammar no pending results will be fired on the Grammar because the event handler throws the events away. + } + + void IRecognizerInternal.SetGrammarWeight(Grammar grammar, float weight) + { + Debug.Assert(grammar != null); + Debug.Assert(grammar.Recognizer == this); + + if (!_supportsSapi53) + { + throw new NotSupportedException(SR.Get(SRID.NotSupportedWithThisVersionOfSAPI2, "Weight")); + } + + InternalGrammarData grammarData = grammar.InternalData; + Debug.Assert(grammarData != null && grammarData._sapiGrammar != null); + + lock (_grammarDataLock) + { + if (grammar.Loaded) + { + if (grammar.IsDictation(grammar.Uri)) + { + grammarData._sapiGrammar.SetDictationWeight(weight); + } + else + { + grammarData._sapiGrammar.SetRuleWeight(grammar.RuleName, 0, weight); + } + } + grammarData._grammarWeight = weight; + } + } + + void IRecognizerInternal.SetGrammarPriority(Grammar grammar, int priority) + { + Debug.Assert(grammar != null); + Debug.Assert(grammar.Recognizer == this); + + if (!_supportsSapi53) + { + throw new NotSupportedException(SR.Get(SRID.NotSupportedWithThisVersionOfSAPI2, "Priority")); + } + + InternalGrammarData grammarData = grammar.InternalData; + Debug.Assert(grammarData != null && grammarData._sapiGrammar != null); + + lock (_grammarDataLock) + { + if (grammar.Loaded) + { + if (grammar.IsDictation(grammar.Uri)) + { + // This is not supported in SAPI currently. + // but not necessarily always. + throw new NotSupportedException(SR.Get(SRID.CannotSetPriorityOnDictation)); + } + else + { + grammarData._sapiGrammar.SetRulePriority(grammar.RuleName, 0, priority); + } + } + grammarData._grammarPriority = priority; + } + } + + // This method is used to get the Grammar object back from the id returned in the sapi recognition events. + Grammar IRecognizerInternal.GetGrammarFromId(ulong id) + { + lock (SapiRecognizer) // Lock to prevent enumerating _grammars from failing if list is modified on main thread + { + foreach (Grammar grammar in _grammars) + { + InternalGrammarData grammarData = grammar.InternalData; + if (grammarData._grammarId == id) + { + Debug.Assert(grammar.State == GrammarState.Loaded && grammar.Recognizer == this); + return grammar; + } + } + } + + return null; // The grammar has already been unloaded + } + + void IRecognizerInternal.SetDictationContext(Grammar grammar, string precedingText, string subsequentText) + { + if (precedingText == null) { precedingText = string.Empty; } + if (subsequentText == null) { subsequentText = string.Empty; } + + SPTEXTSELECTIONINFO selectionInfo = new(0, 0, (uint)precedingText.Length, 0); + string textString = precedingText + subsequentText + "\0\0"; + + SapiGrammar sapiGrammar = grammar.InternalData._sapiGrammar; + sapiGrammar.SetWordSequenceData(textString, selectionInfo); + } + + #endregion + internal RecognitionResult EmulateRecognize(string inputText) + { + Helpers.ThrowIfEmptyOrNull(inputText, nameof(inputText)); + + return InternalEmulateRecognize(inputText, SpeechEmulationCompareFlags.SECFDefault, false, null); + } + internal void EmulateRecognizeAsync(string inputText) + { + Helpers.ThrowIfEmptyOrNull(inputText, nameof(inputText)); + + InternalEmulateRecognizeAsync(inputText, SpeechEmulationCompareFlags.SECFDefault, false, null); + } + internal RecognitionResult EmulateRecognize(string inputText, CompareOptions compareOptions) + { + Helpers.ThrowIfEmptyOrNull(inputText, nameof(inputText)); + + bool defaultCasing = compareOptions == CompareOptions.IgnoreCase || compareOptions == CompareOptions.OrdinalIgnoreCase; + + // In Sapi 5.1 the only option is case-sensitive search with extendedWordFormat checking. + // We still let you use the default EmulateRecognize although the behavior is slightly different. + // Disable additional flags even with SAPI 5.3 until final EmulateRecognition design completed. + if (!_supportsSapi53 && !defaultCasing) + { + // Disable async grammar loading on SAPI 5.1 because of threading model issues. + // Note that even if there are no threading issues, baseUri is not supported with SAPI 5.1. + throw new NotSupportedException(SR.Get(SRID.NotSupportedWithThisVersionOfSAPICompareOption)); + } + + return InternalEmulateRecognize(inputText, ConvertCompareOptions(compareOptions), !defaultCasing, null); + } + internal void EmulateRecognizeAsync(string inputText, CompareOptions compareOptions) + { + Helpers.ThrowIfEmptyOrNull(inputText, nameof(inputText)); + + bool defaultCasing = compareOptions == CompareOptions.IgnoreCase || compareOptions == CompareOptions.OrdinalIgnoreCase; + + // In Sapi 5.1 the only option is case-sensitive search with extendedWordFormat checking. + // We still let you use the default EmulateRecognize although the behavior is slightly different. + // Disable additional flags even with SAPI 5.3 until final EmulateRecognition design completed. + if (!_supportsSapi53 && !defaultCasing) + { + // Disable async grammar loading on SAPI 5.1 because of threading model issues. + // Note that even if there are no threading issues, baseUri is not supported with SAPI 5.1. + throw new NotSupportedException(SR.Get(SRID.NotSupportedWithThisVersionOfSAPICompareOption)); + } + + InternalEmulateRecognizeAsync(inputText, ConvertCompareOptions(compareOptions), !defaultCasing, null); + } + internal RecognitionResult EmulateRecognize(RecognizedWordUnit[] wordUnits, CompareOptions compareOptions) + { + // In Sapi 5.1 the only option is case-sensitive search with extendedWordFormat checking. + // We still let you use the default EmulateRecognize although the behavior is slightly different. + // Disable additional flags even with SAPI 5.3 until final EmulateRecognition design completed. + if (!_supportsSapi53) + { + // Disable async grammar loading on SAPI 5.1 because of threading model issues. + // Note that even if there are no threading issues, baseUri is not supported with SAPI 5.1. + throw new NotSupportedException(SR.Get(SRID.NotSupportedWithThisVersionOfSAPI)); + } + Helpers.ThrowIfNull(wordUnits, nameof(wordUnits)); + + foreach (RecognizedWordUnit wordUnit in wordUnits) + { + if (wordUnit == null) + { + throw new ArgumentException(SR.Get(SRID.ArrayOfNullIllegal), nameof(wordUnits)); + } + } + + return InternalEmulateRecognize(null, ConvertCompareOptions(compareOptions), true, wordUnits); + } + internal void EmulateRecognizeAsync(RecognizedWordUnit[] wordUnits, CompareOptions compareOptions) + { + // In Sapi 5.1 the only option is case-sensitive search with extendedWordFormat checking. + // We still let you use the default EmulateRecognize although the behavior is slightly different. + // Disable additional flags even with SAPI 5.3 until final EmulateRecognition design completed. + if (!_supportsSapi53) + { + // Disable async grammar loading on SAPI 5.1 because of threading model issues. + // Note that even if there are no threading issues, baseUri is not supported with SAPI 5.1. + throw new NotSupportedException(SR.Get(SRID.NotSupportedWithThisVersionOfSAPI)); + } + Helpers.ThrowIfNull(wordUnits, nameof(wordUnits)); + + foreach (RecognizedWordUnit wordUnit in wordUnits) + { + if (wordUnit == null) + { + throw new ArgumentException(SR.Get(SRID.ArrayOfNullIllegal), nameof(wordUnits)); + } + } + + InternalEmulateRecognizeAsync(null, ConvertCompareOptions(compareOptions), true, wordUnits); + } + + // Methods to pause the recognizer to do atomic updates: + internal void RequestRecognizerUpdate() + { + RequestRecognizerUpdate(null); + } + internal void RequestRecognizerUpdate(object userToken) + { + uint bookmarkId = AddBookmarkItem(userToken); + + // This fires the bookmark as soon as possible so we set the time as zero and don't set the SPBO_AHEAD flag. + SapiContext.Bookmark(SPBOOKMARKOPTIONS.SPBO_PAUSE, 0, new IntPtr(bookmarkId)); + } + internal void RequestRecognizerUpdate(object userToken, TimeSpan audioPositionAheadToRaiseUpdate) + { + if (audioPositionAheadToRaiseUpdate < TimeSpan.Zero) + { + throw new NotSupportedException(SR.Get(SRID.NegativeTimesNotSupported)); + } + if (!_supportsSapi53) + { + throw new NotSupportedException(SR.Get(SRID.NotSupportedWithThisVersionOfSAPI)); + } + + uint bookmarkId = AddBookmarkItem(userToken); + + // This always fires the bookmark ahead of the current position. + // So calling this with zero will wait until the recognizer catches up with the current audio position before firing. + SapiContext.Bookmark(SPBOOKMARKOPTIONS.SPBO_PAUSE | SPBOOKMARKOPTIONS.SPBO_AHEAD | SPBOOKMARKOPTIONS.SPBO_TIME_UNITS, + (ulong)audioPositionAheadToRaiseUpdate.Ticks, new IntPtr(bookmarkId)); + } + + internal void Initialize(SapiRecognizer recognizer, bool inproc) + { + // Create RecoContext: + _sapiRecognizer = recognizer; + _inproc = inproc; + + _recoThunk = new RecognizerBaseThunk(this); + + try + { + _sapiContext = _sapiRecognizer.CreateRecoContext(); + } + catch (COMException e) + { + // SAPI 5.1 can throw this error when no recognizer + if (!_supportsSapi53 && (SAPIErrorCodes)e.ErrorCode == SAPIErrorCodes.SPERR_NOT_FOUND) + { + throw new PlatformNotSupportedException(SR.Get(SRID.RecognitionNotSupported)); + } + throw ExceptionFromSapiCreateRecognizerError(e); + } + + // See if SAPI 5.3 features are supported. + _supportsSapi53 = recognizer.IsSapi53; + + if (_supportsSapi53) + { + _sapiContext.SetGrammarOptions(SPGRAMMAROPTIONS.SPGO_ALL); + } + + try + { + ISpPhoneticAlphabetSelection alphabetSelection = _sapiContext as ISpPhoneticAlphabetSelection; + if (alphabetSelection != null) + { + alphabetSelection.SetAlphabetToUPS(true); + } + else + { + Trace.TraceInformation("SAPI does not implement phonetic alphabet selection."); + } + } + catch (COMException) + { + Trace.TraceError("Cannot force SAPI to set the alphabet to UPS"); + } + + _sapiContext.SetAudioOptions(SPAUDIOOPTIONS.SPAO_RETAIN_AUDIO, IntPtr.Zero, IntPtr.Zero); + + // Enable alternates with default max. + MaxAlternates = 10; + + ResetBookmarkTable(); + + // Set basic SR event interests that are routed to the end user. + // Hypothesis and AudioLevelChange events are raised frequently and are less commonly used. + // So their interests will be registered individually. + _eventInterest = (1ul << (int)SPEVENTENUM.SPEI_RESERVED1) | + (1ul << (int)SPEVENTENUM.SPEI_RESERVED2) | + (1ul << (int)SPEVENTENUM.SPEI_START_SR_STREAM) | + (1ul << (int)SPEVENTENUM.SPEI_PHRASE_START) | + (1ul << (int)SPEVENTENUM.SPEI_FALSE_RECOGNITION) | + (1ul << (int)SPEVENTENUM.SPEI_RECOGNITION) | + (1ul << (int)SPEVENTENUM.SPEI_RECO_OTHER_CONTEXT) | + (1ul << (int)SPEVENTENUM.SPEI_END_SR_STREAM) | + (1ul << (int)SPEVENTENUM.SPEI_SR_BOOKMARK); + _sapiContext.SetInterest(_eventInterest, _eventInterest); + + _asyncWorker = new AsyncSerializedWorker(new WaitCallback(DispatchEvents), null); + + _asyncWorkerUI = new AsyncSerializedWorker(null, SynchronizationContext.Current); + _asyncWorkerUI.WorkItemPending += new WaitCallback(SignalHandlerThread); + + _eventNotify = _sapiContext.CreateEventNotify(_asyncWorker, _supportsSapi53); + + _grammars = new List(); + _readOnlyGrammars = new ReadOnlyCollection(_grammars); + UpdateAudioFormat(null); + InitialSilenceTimeout = TimeSpan.FromSeconds(30); + } + + internal void RecognizeAsync(RecognizeMode mode) + { + lock (SapiRecognizer) // Lock to protect _isRecognizing and _haveInputSource + { + if (_isRecognizing) + { + throw new InvalidOperationException(SR.Get(SRID.RecognizerAlreadyRecognizing)); + } + if (!_haveInputSource) + { + throw new InvalidOperationException(SR.Get(SRID.RecognizerNoInputSource)); + } + + _isRecognizing = true; + + // The call to RecognizeAsync may happen before the event for the start stream arrives so remove the assert. + //Debug.Assert (_detectingInitialSilenceTimeout == false); + Debug.Assert(_detectingBabbleTimeout == false); + Debug.Assert(_initialSilenceTimeoutReached == false); + Debug.Assert(_babbleTimeoutReached == false); + Debug.Assert(_isRecognizeCancelled == false); + Debug.Assert(_lastResult == null); + Debug.Assert(_lastException == null); + } // Not recognizing so no events firing - can unlock now + + _recognizeMode = mode; // This is always Multiple for SpeechRecognizer. If Automatic stop after each recognition. + + if (_supportsSapi53) + { + // On another thread - wait for grammar loading to complete and start the recognizer. + if (!ThreadPool.QueueUserWorkItem(new WaitCallback(RecognizeAsyncWaitForGrammarsToLoad))) + { + throw new OperationCanceledException(SR.Get(SRID.OperationAborted)); + } + } + else + { + // Don't support async grammar loading and can't call this on another thread because of threading model issues. + // So just start and throw if there's a problem starting the audio. + try + { + SapiRecognizer.SetRecoState(SPRECOSTATE.SPRST_ACTIVE_ALWAYS); + Debug.WriteLine("Grammar loads completed, recognition started."); + } + catch (COMException comException) + { + Debug.WriteLine("Problem starting recognition - sapi exception."); + throw ExceptionFromSapiStreamError((SAPIErrorCodes)comException.ErrorCode); + } + catch + { + Debug.WriteLine("Problem starting recognition - unknown exception."); + throw; + } + } + } + + internal RecognitionResult Recognize(TimeSpan initialSilenceTimeout) + { + //let InitialSilenceTimeout property below do validation on the TimeSpan parameter + + RecognitionResult result = null; + bool completed = false; + bool hasPendingTask = false; + bool canceled = false; + + EventHandler eventHandler = delegate (object sender, RecognizeCompletedEventArgs eventArgs) + { + result = eventArgs.Result; + completed = true; + }; + + TimeSpan oldInitialSilenceTimeout = _initialSilenceTimeout; + this.InitialSilenceTimeout = initialSilenceTimeout; + + RecognizeCompletedSync += eventHandler; + + //InitialSilenceTimeout bookmark should keep this function from hanging forever, but also have a timeout + //here in case something's wrong with the audio and the bookmark never gets hit. + TimeSpan eventTimeout = TimeSpan.FromTicks(Math.Max(initialSilenceTimeout.Ticks, _defaultTimeout.Ticks)); + + try + { + _asyncWorkerUI.AsyncMode = false; + RecognizeAsync(RecognizeMode.Single); + while (!completed && !_disposed) + { + if (!canceled) + { + hasPendingTask = _handlerWaitHandle.WaitOne(eventTimeout, false); + if (!hasPendingTask) + { + EndRecognitionWithTimeout(); + canceled = true; + } + } + else + { + // We have canceled the recognition, so now we only wait to process remaining events + // until SPEI_END_SR_STREAM event arrives. + hasPendingTask = _handlerWaitHandle.WaitOne(eventTimeout, false); + } + + if (hasPendingTask) + { + _asyncWorkerUI.ConsumeQueue(); + } + } + } + finally + { + RecognizeCompletedSync -= eventHandler; + _initialSilenceTimeout = oldInitialSilenceTimeout; + _asyncWorkerUI.AsyncMode = true; + } + + return result; + } + + internal void RecognizeAsyncCancel() + { + bool doCancel = false; + + lock (SapiRecognizer) // Lock to protect _isRecognizing and _isRecognizeCancelled + { + if (_isRecognizing) + { + if (!_isEmulateRecognition) + { + doCancel = true; + _isRecognizeCancelled = true; // Set this flag so the RecognizeCompleted event shows the operation was cancelled. + } + else + { + // Reset all the recognition flags if an emulate recognition is in progress + _isRecognizing = _isEmulateRecognition = false; + } + } + } + + if (doCancel) + { + // Don't hold the lock while we do this. + try + { + SapiRecognizer.SetRecoState(SPRECOSTATE.SPRST_INACTIVE_WITH_PURGE); + } + catch (COMException e) + { + throw ExceptionFromSapiCreateRecognizerError(e); + } + } + } + + internal void RecognizeAsyncStop() + { + bool doCancel = false; + + lock (SapiRecognizer) // Lock to protect _isRecognizing and _isRecognizeCancelled + { + if (_isRecognizing) + { + doCancel = true; + _isRecognizeCancelled = true; // Still set the flag as this is a kind of cancel. + } + } + + if (doCancel) + { + // Don't hold the lock while we do this. + try + { + SapiRecognizer.SetRecoState(SPRECOSTATE.SPRST_INACTIVE); + } + catch (COMException e) + { + throw ExceptionFromSapiCreateRecognizerError(e); + } + } + } + + // Controls whether the recognizer is paused after each recognition. + // This is always true for the SpeechRecognitionEngine and is customizable {default false} for the SpeechRecognizer. + internal bool PauseRecognizerOnRecognition + { + // No need to lock anything as this value is non-touched in the event handling code and we are only enumerating _grammars on main thread. + get { return _pauseRecognizerOnRecognition; } + set + { + if (value != _pauseRecognizerOnRecognition) + { + _pauseRecognizerOnRecognition = value; + + lock (SapiRecognizer) + { + foreach (Grammar grammar in _grammars) + { + SapiGrammar sapiGrammar = grammar.InternalData._sapiGrammar; + ActivateRule(sapiGrammar, grammar.Uri, grammar.RuleName); + } + } + } + } + } + + /// + /// Set the current input for the recognizer to a file + /// + internal void SetInput(string path) + { + Stream inputStream = new FileStream(path, FileMode.Open, FileAccess.Read, FileShare.Read); + SetInput(inputStream, null); + + // Keep track of the local stream + _inputStream = inputStream; + } + + /// + /// Set the current input for the recognizer to a file + /// + internal void SetInput(Stream stream, SpeechAudioFormatInfo audioFormat) + { + lock (SapiRecognizer) // Lock to protect _isRecognizing and _haveInputSource + { + if (_isRecognizing) + { + throw new InvalidOperationException(SR.Get(SRID.RecognizerAlreadyRecognizing)); + } + + try + { + // Detach the input stream from the recognizer + if (stream == null) + { + SapiRecognizer.SetInput(null, false); + _haveInputSource = false; + } + else + { + SapiRecognizer.SetInput(new SpAudioStreamWrapper(stream, audioFormat), false); + _haveInputSource = true; + } + } + catch (COMException e) + { + throw ExceptionFromSapiCreateRecognizerError(e); + } + + CloseCachedInputStream(); + UpdateAudioFormat(audioFormat); + } + } + + /// + /// Reset the recognizer input stream to the default audio device + /// + internal void SetInputToDefaultAudioDevice() + { + lock (SapiRecognizer) // Lock to protect _isRecognizing and _haveInputSource + { + if (_isRecognizing) + { + throw new InvalidOperationException(SR.Get(SRID.RecognizerAlreadyRecognizing)); + } + + ISpObjectTokenCategory category = (ISpObjectTokenCategory)new SpObjectTokenCategory(); + try + { + category.SetId(SAPICategories.AudioIn, false); + + string tokenId; + category.GetDefaultTokenId(out tokenId); + + ISpObjectToken token = (ISpObjectToken)new SpObjectToken(); + try + { + token.SetId(null, tokenId, false); + SapiRecognizer.SetInput(token, true); + } + catch (COMException e) + { + throw ExceptionFromSapiCreateRecognizerError(e); + } + finally + { + Marshal.ReleaseComObject(token); + } + } + catch (COMException e) + { + throw ExceptionFromSapiCreateRecognizerError(e); + } + finally + { + Marshal.ReleaseComObject(category); + } + + UpdateAudioFormat(null); + _haveInputSource = true; // On success + } + } + + internal int QueryRecognizerSettingAsInt(string settingName) + { + Helpers.ThrowIfEmptyOrNull(settingName, nameof(settingName)); + + // See if property is an int. + return SapiRecognizer.GetPropertyNum(settingName); + } + + internal object QueryRecognizerSetting(string settingName) + { + Helpers.ThrowIfEmptyOrNull(settingName, nameof(settingName)); + + // See if property is an int. + try + { + return SapiRecognizer.GetPropertyNum(settingName); + } + catch (Exception e) + { + if (e is COMException || e is InvalidOperationException || e is KeyNotFoundException) + { + return SapiRecognizer.GetPropertyString(settingName); + } + throw; + } + } + + internal void UpdateRecognizerSetting(string settingName, string updatedValue) + { + Helpers.ThrowIfEmptyOrNull(settingName, nameof(settingName)); + + SapiRecognizer.SetPropertyString(settingName, updatedValue); + } + + internal void UpdateRecognizerSetting(string settingName, int updatedValue) + { + Helpers.ThrowIfEmptyOrNull(settingName, nameof(settingName)); + + SapiRecognizer.SetPropertyNum(settingName, updatedValue); + } + + internal static Exception ExceptionFromSapiCreateRecognizerError(COMException e) + { + return ExceptionFromSapiCreateRecognizerError((SAPIErrorCodes)e.ErrorCode); + } + + internal static Exception ExceptionFromSapiCreateRecognizerError(SAPIErrorCodes errorCode) + { + SRID srid = SapiConstants.SapiErrorCode2SRID(errorCode); + switch (errorCode) + { + case SAPIErrorCodes.CLASS_E_CLASSNOTAVAILABLE: + case SAPIErrorCodes.REGDB_E_CLASSNOTREG: + { + OperatingSystem OS = Environment.OSVersion; + if (IntPtr.Size == 8 && // 64-bit system + OS.Platform == PlatformID.Win32NT && // On Windows NT or above + OS.Version.Major == 5) // Windows 2000 / XP / Server 2003 + { + return new NotSupportedException(SR.Get(SRID.RecognitionNotSupportedOn64bit)); + } + else + { + return new PlatformNotSupportedException(SR.Get(SRID.RecognitionNotSupported)); + } + } + + case SAPIErrorCodes.SPERR_SHARED_ENGINE_DISABLED: + case SAPIErrorCodes.SPERR_RECOGNIZER_NOT_FOUND: + return new PlatformNotSupportedException(SR.Get(srid)); + + default: + Exception exReturn = null; ; + if (srid >= 0) + { + exReturn = new InvalidOperationException(SR.Get(srid)); + } + else + { + try + { + Marshal.ThrowExceptionForHR((int)errorCode); + } + catch (Exception ex) + { + exReturn = ex; + } + } + return exReturn; + } + } + + #endregion + + #region Internal Properties + + // Note on locking implementation: + // + // In general operations are not locked on the RecognizerBase - there's no single lock that makes everything thread safe. + // This is the normal .NET design pattern. + // + // However, because there is processing of sapi events, going on different threads that the app does not control, + // we need to protect certain members. + // + // This is generally done with "lock (SapiRecognizer)" - the choice of SapiRecognizer is arbitrary - any object could have been used. + // Anything that's touched both by sapi event code and by public methods need this lock. + // {For sanity this includes bool like _isRecognizing even though setting these is atomic}. + // Similarly when enumerating the Grammars collection we need to ensure no other thread can be adding or removing items. + // + // Some other well encapsulated fields also lock themselves e.g. the bookmark table. + // + // In addition, the EventNotify class holds a lock to prevent events being fired more that one at a time. + // It is required that Dispose also takes this lock. + + internal TimeSpan InitialSilenceTimeout + { + // lock to protect _initialSilenceTimeout and _isRecognizing + get { lock (SapiRecognizer) { return _initialSilenceTimeout; } } + set + { + if (value < TimeSpan.Zero) + { + throw new ArgumentOutOfRangeException(nameof(value), SR.Get(SRID.NegativeTimesNotSupported)); + } + + lock (SapiRecognizer) + { + if (_isRecognizing) + { + throw new InvalidOperationException(SR.Get(SRID.RecognizerAlreadyRecognizing)); + } + _initialSilenceTimeout = value; + } + } + } + + internal TimeSpan BabbleTimeout + { + // lock to protect _babbleTimeout and _isRecognizing + get { lock (SapiRecognizer) { return _babbleTimeout; } } + set + { + if (value < TimeSpan.Zero) + { + throw new ArgumentOutOfRangeException(nameof(value), SR.Get(SRID.NegativeTimesNotSupported)); + } + + lock (SapiRecognizer) + { + if (_isRecognizing) + { + throw new InvalidOperationException(SR.Get(SRID.RecognizerAlreadyRecognizing)); + } + _babbleTimeout = value; + } + } + } + + internal RecognizerState State + { + get + { + try + { + SPRECOSTATE sapiState; + sapiState = SapiRecognizer.GetRecoState(); // This does not wait for engine sync point so should be fast. + if (sapiState == SPRECOSTATE.SPRST_ACTIVE || sapiState == SPRECOSTATE.SPRST_ACTIVE_ALWAYS) + { + return RecognizerState.Listening; + } + else + { + return RecognizerState.Stopped; + } + } + catch (COMException e) + { + throw ExceptionFromSapiCreateRecognizerError(e); + } + } + } + + internal bool Enabled + { + get + { + lock (SapiRecognizer) // Lock to protect _enabled + { + return _enabled; + } + } + set + { + lock (SapiRecognizer) // Lock to protect _enabled + { + if (value != _enabled) + { + try + { + SapiContext.SetContextState(value ? SPCONTEXTSTATE.SPCS_ENABLED : SPCONTEXTSTATE.SPCS_DISABLED); + _enabled = value; + } + catch (COMException e) + { + throw ExceptionFromSapiCreateRecognizerError(e); + } + } + } + } + } + + // Gives access to the collection of grammars that are currently active. Read-only. + internal ReadOnlyCollection Grammars + { + get { return _readOnlyGrammars; } + } + + // Gives access to the set of attributes exposed by this recognizer. + internal RecognizerInfo RecognizerInfo + { + get + { + if (_recognizerInfo == null) + { + try + { + _recognizerInfo = SapiRecognizer.GetRecognizerInfo(); + } + catch (COMException e) + { + throw ExceptionFromSapiCreateRecognizerError(e); + } + } + + return _recognizerInfo; + } + } + + // Data on the audio stream the recognizer is processing + internal AudioState AudioState + { + get + { + if (!_haveInputSource) + { + // If we don't have an audio source return an empty status. + return AudioState.Stopped; + } + return _audioState; + } + set + { + _audioState = value; + } + } + + internal int AudioLevel + { + get + { + // If we don't have an audio source return 0 + int level = 0; + if (_haveInputSource) + { + SPRECOGNIZERSTATUS recoStatus; + + try + { + // These calls do not wait for engine sync point so should be fast. + recoStatus = SapiRecognizer.GetStatus(); + + lock (SapiRecognizer) // Lock to protect _audioStatus. + { + if (_supportsSapi53) + { + level = (int)recoStatus.AudioStatus.dwAudioLevel; + } + else + { + level = 0; // This is not implemented in SAPI 5.1 so will always be zero. + } + } + } + catch (COMException e) + { + throw ExceptionFromSapiCreateRecognizerError(e); + } + } + + return level; + } + } + + internal TimeSpan AudioPosition + { + get + { + if (!_haveInputSource) + { + // If we don't have an audio source return an empty status. + return TimeSpan.Zero; + } + + SPRECOGNIZERSTATUS recoStatus; + + try + { + // These calls do not wait for engine sync point so should be fast. + recoStatus = SapiRecognizer.GetStatus(); + + lock (SapiRecognizer) // Lock to protect _audioStatus. + { + SpeechAudioFormatInfo audioFormat = AudioFormat; + return audioFormat.AverageBytesPerSecond > 0 ? new TimeSpan((long)((recoStatus.AudioStatus.CurDevicePos * TimeSpan.TicksPerSecond) / (ulong)audioFormat.AverageBytesPerSecond)) : TimeSpan.Zero; + } + } + catch (COMException e) + { + throw ExceptionFromSapiCreateRecognizerError(e); + } + } + } + + internal TimeSpan RecognizerAudioPosition + { + get + { + if (!_haveInputSource) + { + // If we don't have an audio source return an empty status. + return TimeSpan.Zero; + } + + SPRECOGNIZERSTATUS recoStatus; + + try + { + // These calls do not wait for engine sync point so should be fast. + recoStatus = SapiRecognizer.GetStatus(); + + lock (SapiRecognizer) // Lock to protect _audioStatus. + { + // RecognizerPosition and AudioPosition get reset to zero at the start of each stream so can be used directly. + return new TimeSpan((long)recoStatus.ullRecognitionStreamTime); + } + } + catch (COMException e) + { + throw ExceptionFromSapiCreateRecognizerError(e); + } + } + } + internal SpeechAudioFormatInfo AudioFormat + { + get + { + lock (SapiRecognizer) // Lock to protect _audioFormat and _haveInputSource + { + if (!_haveInputSource) + { + // If we don't have an audio source trying to return data about the audio doesn't make sense. + return null; + } + + if (_audioFormat == null) + { + _audioFormat = GetSapiAudioFormat(); + } + } + return _audioFormat; + } + } + internal int MaxAlternates + { + get { return _maxAlternates; } + set + { + if (value < 0) + { + throw new ArgumentOutOfRangeException(nameof(value), SR.Get(SRID.MaxAlternatesInvalid)); + } + if (value != _maxAlternates) + { + SapiContext.SetMaxAlternates((uint)value); + _maxAlternates = value; // On success + } + } + } + + #endregion + + #region Internal Events + + // Internal event used to hook up the SpeechRecognitionEngine RecognizeCompleted event. + internal event EventHandler RecognizeCompleted; + + // Fired when the RecognizeAsync process completes. + internal event EventHandler EmulateRecognizeCompleted; + + // Internal event used to hook up the SpeechRecognizer StateChanged event. + internal event EventHandler StateChanged; + internal event EventHandler LoadGrammarCompleted; + + // The event fired when speech is detected. Used for barge-in. + internal event EventHandler SpeechDetected; + + // The event fired on a recognition. + internal event EventHandler SpeechRecognized; + + // The event fired on a no recognition + internal event EventHandler SpeechRecognitionRejected; + +#pragma warning disable 6504 + // Occurs when a spoken phrase is partially recognized. + internal event EventHandler SpeechHypothesized + { + [MethodImplAttribute(MethodImplOptions.Synchronized)] + add + { + if (_speechHypothesizedDelegate == null) + { + AddEventInterest(1ul << (int)SPEVENTENUM.SPEI_HYPOTHESIS); + } + _speechHypothesizedDelegate += value; + } + + [MethodImplAttribute(MethodImplOptions.Synchronized)] + remove + { + _speechHypothesizedDelegate -= value; + if (_speechHypothesizedDelegate == null) + { + RemoveEventInterest(1ul << (int)SPEVENTENUM.SPEI_HYPOTHESIS); + } + } + } + internal event EventHandler AudioSignalProblemOccurred + { + [MethodImplAttribute(MethodImplOptions.Synchronized)] + add + { + if (_audioSignalProblemOccurredDelegate == null) + { + AddEventInterest(1ul << (int)SPEVENTENUM.SPEI_INTERFERENCE); + } + _audioSignalProblemOccurredDelegate += value; + } + + [MethodImplAttribute(MethodImplOptions.Synchronized)] + remove + { + _audioSignalProblemOccurredDelegate -= value; + if (_audioSignalProblemOccurredDelegate == null) + { + RemoveEventInterest(1ul << (int)SPEVENTENUM.SPEI_INTERFERENCE); + } + } + } + internal event EventHandler AudioLevelUpdated + { + [MethodImplAttribute(MethodImplOptions.Synchronized)] + add + { + if (_audioLevelUpdatedDelegate == null) + { + AddEventInterest(1ul << (int)SPEVENTENUM.SPEI_SR_AUDIO_LEVEL); + } + _audioLevelUpdatedDelegate += value; + } + + [MethodImplAttribute(MethodImplOptions.Synchronized)] + remove + { + _audioLevelUpdatedDelegate -= value; + if (_audioLevelUpdatedDelegate == null) + { + RemoveEventInterest(1ul << (int)SPEVENTENUM.SPEI_SR_AUDIO_LEVEL); + } + } + } + internal event EventHandler AudioStateChanged + { + [MethodImplAttribute(MethodImplOptions.Synchronized)] + add + { + _audioStateChangedDelegate += value; + } + + [MethodImplAttribute(MethodImplOptions.Synchronized)] + remove + { + _audioStateChangedDelegate -= value; + } + } + +#pragma warning restore 6504 + internal event EventHandler RecognizerUpdateReached; + + #endregion + + #region Protected Methods + protected virtual void Dispose(bool disposing) + { + if (!_disposed) + { + if (disposing) + { + // Lock to wait for event dispatching to finish + lock (_thisObjectLock) + { + // Make sure no pending posts are sent, no events are dispatched as we are disposing + + if (_asyncWorkerUI != null) + { + _asyncWorkerUI.Enabled = false; + _asyncWorkerUI.Purge(); + _asyncWorker.Enabled = false; + _asyncWorker.Purge(); + } + + // Dispose unmanaged resources in event notification and detach from ISpEventSource. + // Release grammar resources. + if (_sapiContext != null) + { + _sapiContext.DisposeEventNotify(_eventNotify); + _handlerWaitHandle.Close(); + UnloadAllGrammars(); + _waitForGrammarsToLoad.Dispose(); + } + CloseCachedInputStream(); + + // Release SAPI recognizer/recoContext interfaces. + // We do not need to release additional references copy onto the same RCW. + if (_sapiContext != null) + { + _sapiContext.Dispose(); + _sapiContext = null; + } + if (_sapiRecognizer != null) + { + _sapiRecognizer.Dispose(); + _sapiRecognizer = null; + } + + if (_recognizerInfo != null) + { + _recognizerInfo.Dispose(); + _recognizerInfo = null; + } + + _disposed = true; + } + } + } + } + + #endregion + + #region Private Properties + + // Properties to get access to the underlying SAPI objects and to throw if disposed. + + private SapiRecoContext SapiContext + { + // Also this method is not public. +#pragma warning disable 6503 + get { if (_disposed) { throw new ObjectDisposedException("RecognizerBase"); } return _sapiContext; } +#pragma warning restore 6503 + } + + private SapiRecognizer SapiRecognizer + { +#pragma warning disable 6503 + get { if (_disposed) { throw new ObjectDisposedException("RecognizerBase"); } return _sapiRecognizer; } +#pragma warning restore 6503 + } + + #endregion + + #region Private Methods + + // Method called from LoadGrammar and LoadGrammarAsync to load the data from a Grammar into sapiGrammar. + // Grammar is unchanged by this method. + private void LoadSapiGrammar(Grammar grammar, SapiGrammar sapiGrammar, bool enabled, float weight, int priority) + { + Uri baseUri = grammar.BaseUri; + + if (_supportsSapi53 && baseUri == null && grammar.Uri != null) + { + // If the base Uri has not been set any other way, then set the base Uri for this file + string uri = grammar.Uri.OriginalString; + int posSlash = uri.LastIndexOfAny(new char[] { '\\', '/' }); + if (posSlash >= 0) + { + baseUri = new Uri(uri.Substring(0, posSlash + 1), UriKind.RelativeOrAbsolute); + } + } + + // For dictation grammar, pass the Uri to SAPI. + // For anything else, load it locally to figure out if it is a + // strongly typed grammar. + if (grammar.IsDictation(grammar.Uri)) + { + // If uri load + LoadSapiDictationGrammar(sapiGrammar, grammar.Uri, grammar.RuleName, enabled, weight, priority); + return; + } + LoadSapiGrammarFromCfg(sapiGrammar, grammar, baseUri, enabled, weight, priority); + } + + // Actually load the uri into the sapiGrammar. This does not touch the Grammar object or InternalGrammarData. + // This must be called on a new SapiGrammar that does not already have a grammar loaded {for SetSapiGrammarProperties}. + private void LoadSapiDictationGrammar(SapiGrammar sapiGrammar, Uri uri, string ruleName, bool enabled, float weight, int priority) + { + try + { + if (Grammar.IsDictationGrammar(uri)) + { + // Note: checking whether the grammar is a dictation grammar is somewhat messy. + // This is done because SAPI has different methods to load and activate dictation as it does CFGs. + // Other options here include: + // - Modify SAPI so LoadCmdFromFile works with dictation Uris. + // - Modify the engine and use a regular grammar with a special ruleref to dictation. + // - Call back to the Grammar and let it manage the loading activation. + string topicName = string.IsNullOrEmpty(uri.Fragment) ? null : uri.Fragment.Substring(1, uri.Fragment.Length - 1); + sapiGrammar.LoadDictation(topicName, SPLOADOPTIONS.SPLO_STATIC); + } + else + { + System.Diagnostics.Debug.Assert(false); + } + } + catch (COMException e) + { + switch ((SAPIErrorCodes)e.ErrorCode) + { + case SAPIErrorCodes.SPERR_NOT_FOUND: + { + throw new ArgumentException(SR.Get(SRID.DictationTopicNotFound, uri), e); + } + + default: + { + ThrowIfSapiErrorCode((SAPIErrorCodes)e.ErrorCode); + throw; + } + } + } + + SetSapiGrammarProperties(sapiGrammar, uri, ruleName, enabled, weight, priority); + } + + #region Resource loader implementation + + /// + /// Called to load a grammar and all of its dependent rule refs. + /// + /// Returns the CFG data for a given file and builds a tree of rule ref dependencies. + /// + int ISpGrammarResourceLoader.LoadResource(string bstrResourceUri, bool fAlwaysReload, out IStream pStream, ref string pbstrMIMEType, ref short pfModified, ref string pbstrRedirectUrl) + { + try + { + // Look for the OnInitParameters + int posGreaterThan = bstrResourceUri.IndexOf('>'); + string onInitParameters = null; + if (posGreaterThan > 0) + { + onInitParameters = bstrResourceUri.Substring(posGreaterThan + 1); + bstrResourceUri = bstrResourceUri.Substring(0, posGreaterThan); + } + + // Hack to get the parent and children grammar. + string ruleName = pbstrMIMEType; + + // The parent is the first + string[] ids = pbstrRedirectUrl.Split(new char[] { ' ' }, StringSplitOptions.None); + System.Diagnostics.Debug.Assert(ids.Length == 2); + + uint parentGrammarId = uint.Parse(ids[0], CultureInfo.InvariantCulture); + uint grammarId = uint.Parse(ids[1], CultureInfo.InvariantCulture); + + // Create the grammar for that resources. + Uri redirectedUri; + Grammar grammar = Grammar.Create(bstrResourceUri, ruleName, onInitParameters, out redirectedUri); + + // If http:// then set the redirect Uri + if (redirectedUri != null) + { + pbstrRedirectUrl = redirectedUri.ToString(); + } + + // Could fail for SRGS + if (grammar == null) + { + throw new FormatException(SR.Get(SRID.SapiErrorRuleNotFound2, ruleName, bstrResourceUri)); + } + + // Save the SAPI grammar id for that grammar + grammar.SapiGrammarId = grammarId; + + // Find the grammar this ruleref belongs to and add it to the appropriate grammar + Grammar parent = _topLevel.Find(parentGrammarId); + if (parent == null) + { + _topLevel.AddRuleRef(grammar, grammarId); + } + else + { + parent.AddRuleRef(grammar, grammarId); + } + + // Must return and IStream to enable SAPI to retrieve the data + MemoryStream stream = new(grammar.CfgData); + SpStreamWrapper spStream = new(stream); + pStream = spStream; + pfModified = 0; + + return 0; + } + catch (Exception e) + { + // Something failed. + // Save the exception and return an error to SAPI. + pStream = null; + _loadException = e; + return (int)SAPIErrorCodes.SPERR_INVALID_IMPORT; + } + } + + /// + /// Unused + /// + string ISpGrammarResourceLoader.GetLocalCopy(Uri resourcePath, out string mimeType, out Uri redirectUrl) + { + redirectUrl = null; + mimeType = null; + return null; + } + + /// + /// Unused + /// + void ISpGrammarResourceLoader.ReleaseLocalCopy(string path) + { + } + + #endregion + + // Actually load the stream into the sapiGrammar. This does not touch the Grammar object or InternalGrammarData. + // This must be called on a new SapiGrammar that does not already have a grammar loaded {for SetSapiGrammarProperties}. + private void LoadSapiGrammarFromCfg(SapiGrammar sapiGrammar, Grammar grammar, Uri baseUri, bool enabled, float weight, int priority) + { + byte[] data = grammar.CfgData; + + // Pin the array: + GCHandle gcHandle = GCHandle.Alloc(data, GCHandleType.Pinned); + IntPtr dataPtr = gcHandle.AddrOfPinnedObject(); + + // Load the data into SAPI: + try + { + if (_supportsSapi53) + { + _loadException = null; + _topLevel = grammar; + + if (_inproc) + { + // Use the resource loader for Sapi 5.3 and above + // The rulerefs will be resolved locally. + + sapiGrammar.SetGrammarLoader(_recoThunk); + } + sapiGrammar.LoadCmdFromMemory2(dataPtr, SPLOADOPTIONS.SPLO_STATIC, null, baseUri == null ? null : baseUri.ToString()); + } + else + { + sapiGrammar.LoadCmdFromMemory(dataPtr, SPLOADOPTIONS.SPLO_STATIC); + } + } + catch (COMException e) + { + switch ((SAPIErrorCodes)e.ErrorCode) + { + case SAPIErrorCodes.SPERR_UNSUPPORTED_FORMAT: + { + throw new FormatException(SR.Get(SRID.RecognizerInvalidBinaryGrammar), e); + } + case SAPIErrorCodes.SPERR_INVALID_IMPORT: + { + throw new FormatException(SR.Get(SRID.SapiErrorInvalidImport), e); + } + case SAPIErrorCodes.SPERR_TOO_MANY_GRAMMARS: + { + throw new NotSupportedException(SR.Get(SRID.SapiErrorTooManyGrammars), e); + } + case SAPIErrorCodes.SPERR_NOT_FOUND: + { + throw new FileNotFoundException(SR.Get(SRID.ReferencedGrammarNotFound), e); + } + + case ((SAPIErrorCodes)(-1)): + if (_loadException != null) + { + ExceptionDispatchInfo.Throw(_loadException); + } + ThrowIfSapiErrorCode((SAPIErrorCodes)e.ErrorCode); + break; + + default: + ThrowIfSapiErrorCode((SAPIErrorCodes)e.ErrorCode); + break; + } + throw; + } + catch (ArgumentException e) + { + throw new FormatException(SR.Get(SRID.RecognizerInvalidBinaryGrammar), e); + } + finally + { + gcHandle.Free(); + } + + SetSapiGrammarProperties(sapiGrammar, null, grammar.RuleName, enabled, weight, priority); + } + + // Update a new SAPI grammar with relevant enabled, weight and priority and activate the desired rule. + // SetRuleState on the rule is always set to active - theSetGrammarState API is used to enable or disable the grammar. + // This needs to be a new grammar only because it only bothers to update the values of they are different to the default. + private void SetSapiGrammarProperties(SapiGrammar sapiGrammar, Uri uri, string ruleName, bool enabled, float weight, int priority) + { + if (!enabled) + { + // SetGrammarState is ENABLED by default so only call if changed. + sapiGrammar.SetGrammarState(SPGRAMMARSTATE.SPGS_DISABLED); + } + + if (_supportsSapi53) + { + if (priority != 0) + { + if (Grammar.IsDictationGrammar(uri)) + { + throw new NotSupportedException(SR.Get(SRID.CannotSetPriorityOnDictation)); + } + else + { + sapiGrammar.SetRulePriority(ruleName, 0, priority); + } + } + if (!weight.Equals(1.0f)) + { + if (Grammar.IsDictationGrammar(uri)) + { + sapiGrammar.SetDictationWeight(weight); + } + else + { + sapiGrammar.SetRuleWeight(ruleName, 0, weight); + } + } + } + else if (priority != 0 || !weight.Equals(1.0f)) + { + throw new NotSupportedException(SR.Get(SRID.NotSupportedWithThisVersionOfSAPI)); + } + + // Always activate the rule + // Do this after calling SetGrammarState so we don't accidentally enable the Grammar for recognition. + ActivateRule(sapiGrammar, uri, ruleName); + } + + // Method called on background thread to do actual grammar loading. +#pragma warning disable 56500 // Transferring exceptions to another thread + private void LoadGrammarAsyncCallback(object grammarObject) + { + Debug.WriteLine("Loading grammar asynchronously."); + + // Note all of the items called on Grammar are simple properties so we don't + // have any special locking even though this method could be called on different threads. + + Grammar grammar = (Grammar)grammarObject; + InternalGrammarData grammarData = grammar.InternalData; + + // Right now you can't unload a grammar while it is being loaded, so the state must still be being "Loading" + Debug.Assert(grammar.State == GrammarState.Loading); + Debug.Assert(grammar.Recognizer == this); + Debug.Assert(grammarData != null && grammarData._sapiGrammar != null); + + // Now load the grammar: + + // Keep track of any exceptions which we will store in the completed event args. + Exception exception = null; + try + { + // Take the lock here so if an app is updating properties on the grammar at this point on the main thread, + // then the value is pulled and sapi updated atomically. + // Note: This locks properties like Grammar.Enabled so if they are called while an async Grammar load is + // in progress then they will block. This is probably okay for System.Speech, and could be avoided + // by removing the actual call to load the grammar into sapi out of the lock. + lock (_grammarDataLock) + { + // The sapi grammar has already been created, so load the grammar data into SAPI: + LoadSapiGrammar(grammar, grammarData._sapiGrammar, + grammarData._grammarEnabled, grammarData._grammarWeight, grammarData._grammarPriority); + + // Successful load - set the state: + grammar.State = GrammarState.Loaded; + } + + Debug.WriteLine("Finished Loading grammar asynchronously."); + } + catch (Exception e) + { + exception = e; + } + finally + { + if (exception != null) + { + Debug.WriteLine("Failed to load grammar asynchronously."); + + // Need to do special logic to add grammar to collection but with LoadFailed state. + grammar.State = GrammarState.LoadFailed; + grammar.LoadException = exception; + // Wait until UnloadGrammar to release the sapi grammar object. + } + + // Always release reader lock so if RecognizeAsync wants to start it can do so + _waitForGrammarsToLoad.FinishOperation(); + + // Always fire completed event + _asyncWorkerUI.PostOperation(new WaitCallback(LoadGrammarAsyncCompletedCallback), grammarObject); + } + } + +#pragma warning restore 56500 + + // Method called by AsyncOperationManager on appropriate thread when async grammar loading completes. + private void LoadGrammarAsyncCompletedCallback(object grammarObject) + { + Debug.WriteLine("Raising LoadGrammarCompleted event."); + + Grammar grammar = (Grammar)grammarObject; + EventHandler loadGrammarCompletedHandler = LoadGrammarCompleted; + if (loadGrammarCompletedHandler != null) + { + // When a LoadGrammarAsync completes all we must do is raise the LoadGrammarCompleted event. + loadGrammarCompletedHandler(this, new LoadGrammarCompletedEventArgs(grammar, grammar.LoadException, false, null)); + } + } + + // Create a new sapi grammarId and SapiGrammar object. + // The algorithm starts at '1' and increments. + // Eventually the numbers wrap around so you'll end up at 0 etc. which is fine. + // We also check if a value is in use and then skip it. + private SapiGrammar CreateNewSapiGrammar(out ulong grammarId) + { + ulong initialGrammarIdValue = _currentGrammarId; + // No need to lock as enumerating _grammars on the main thread and only gets altered on the main thread + do + { + _currentGrammarId++; + + bool foundCollision = false; + lock (SapiRecognizer) + { + foreach (Grammar g in _grammars) + { + if (_currentGrammarId == g.InternalData._grammarId) + { + // This can only be hit if _currentGrammarId has wrapped around past 2^64. + foundCollision = true; + break; + } + } + } + if (!foundCollision) + { + SapiGrammar sapiGrammar = SapiContext.CreateGrammar(_currentGrammarId); + grammarId = _currentGrammarId; + return sapiGrammar; + } + } + while (_currentGrammarId != initialGrammarIdValue); + + // This is not a realistic scenario because you'd need to have 2^64 grammars loaded to hit this, but it removes at least + // a theoretical infinite loop. + throw new InvalidOperationException(SR.Get(SRID.SapiErrorTooManyGrammars)); + } + + // Do some basic parameter validation on a passed in Grammar + private void ValidateGrammar(Grammar grammar, params GrammarState[] validStates) + { + Helpers.ThrowIfNull(grammar, nameof(grammar)); + + // Check if grammar is in a valid state for the caller. + foreach (GrammarState state in validStates) + { + if (grammar.State == state) + { + // Grammar is in a valid state, but is this the right Recognizer? + if (grammar.State != GrammarState.Unloaded && grammar.Recognizer != this) + { + throw new InvalidOperationException(SR.Get(SRID.GrammarWrongRecognizer)); + } + + // Everything is fine - return. + return; + } + } + + // Grammar was not in correct state - produce exception. + switch (grammar.State) + { + case GrammarState.Unloaded: + throw new InvalidOperationException(SR.Get(SRID.GrammarNotLoaded)); + case GrammarState.Loading: + throw new InvalidOperationException(SR.Get(SRID.GrammarLoadingInProgress)); + case GrammarState.LoadFailed: + throw new InvalidOperationException(SR.Get(SRID.GrammarLoadFailed)); + case GrammarState.Loaded: + throw new InvalidOperationException(SR.Get(SRID.GrammarAlreadyLoaded)); + } + } + + private RecognitionResult InternalEmulateRecognize(string phrase, SpeechEmulationCompareFlags flag, bool useReco2, RecognizedWordUnit[] wordUnits) + { + RecognitionResult result = null; + bool completed = false; + EventHandler eventHandler = delegate (object sender, EmulateRecognizeCompletedEventArgs eventArgs) + { + result = eventArgs.Result; + completed = true; + }; + + EmulateRecognizeCompletedSync += eventHandler; + + try + { + _asyncWorkerUI.AsyncMode = false; + InternalEmulateRecognizeAsync(phrase, flag, useReco2, wordUnits); + do + { + _handlerWaitHandle.WaitOne(); + _asyncWorkerUI.ConsumeQueue(); + } while (!completed && !_disposed); + } + finally + { + EmulateRecognizeCompletedSync -= eventHandler; + _asyncWorkerUI.AsyncMode = true; + } + + return result; + } + + // Pass the Emulation information to SAPI + private void InternalEmulateRecognizeAsync(string phrase, SpeechEmulationCompareFlags flag, bool useReco2, RecognizedWordUnit[] wordUnits) + { + lock (SapiRecognizer) // Lock to protect _isRecognizing and _haveInputSource + { + if (_isRecognizing) + { + throw new InvalidOperationException(SR.Get(SRID.RecognizerAlreadyRecognizing)); + } + + _isRecognizing = true; + _isEmulateRecognition = true; + } // Not recognizing so no events firing - can unlock now + + if (useReco2 || _supportsSapi53) + { + // Create the structure to pass the recognition engine. + IntPtr data; + GCHandle[] memHandles = null; + ISpPhrase iSpPhrase = null; + + if (wordUnits == null) + { + iSpPhrase = SPPHRASE.CreatePhraseFromText(phrase.Trim(), RecognizerInfo.Culture, out memHandles, out data); + } + else + { + iSpPhrase = SPPHRASE.CreatePhraseFromWordUnits(wordUnits, RecognizerInfo.Culture, out memHandles, out data); + } + + try + { + SAPIErrorCodes hr = SapiRecognizer.EmulateRecognition(iSpPhrase, (uint)(flag)); + if (hr != SAPIErrorCodes.S_OK) + { + EmulateRecognizedFailReportError(hr); + } + } + finally + { + foreach (GCHandle memHandle in memHandles) + { + memHandle.Free(); + } + Marshal.FreeCoTaskMem(data); + } + } + else + { + // Fast case + SAPIErrorCodes hr = SapiRecognizer.EmulateRecognition(phrase); + if (hr != SAPIErrorCodes.S_OK) + { + EmulateRecognizedFailReportError(hr); + } + } + } + + private void EmulateRecognizedFailReportError(SAPIErrorCodes hr) + { + _lastException = ExceptionFromSapiCreateRecognizerError(hr); + + // + // Do not fire the recognize completed event if we know that we will receive + // a recognition event eventually; as doing so will lead to premature completion + // of the recognition task without raising any recognition events. + // + + // + // We do not have recognition event for SP_NO_ACTIVE_RULE (thus should complete immediately), + // but we have (false) recognition for the other two SP_NO_PARSE_FOUND and S_FALSE. + // + if ((int)hr < 0 || hr == SAPIErrorCodes.SP_NO_RULE_ACTIVE) + { + FireEmulateRecognizeCompletedEvent(null, _lastException, true); + } + } + + // Set the desired rule to either the active or active_with_auto_pause state. + // This method is used when a grammar is first loaded, and if the PauseRecognizerOnRecognition property is changed. + private void ActivateRule(SapiGrammar sapiGrammar, Uri uri, string ruleName) + { + SPRULESTATE ruleState = _pauseRecognizerOnRecognition ? SPRULESTATE.SPRS_ACTIVE_WITH_AUTO_PAUSE : SPRULESTATE.SPRS_ACTIVE; + + SAPIErrorCodes errorCode; + if (Grammar.IsDictationGrammar(uri)) + { + errorCode = sapiGrammar.SetDictationState(ruleState); + } + else + { + errorCode = sapiGrammar.SetRuleState(ruleName, ruleState); + } + + if (errorCode == SAPIErrorCodes.SPERR_NOT_TOPLEVEL_RULE || errorCode == SAPIErrorCodes.SP_NO_RULES_TO_ACTIVATE) + { + if (uri == null) + { + if (string.IsNullOrEmpty(ruleName)) + { + throw new FormatException(SR.Get(SRID.RecognizerNoRootRuleToActivate)); + } + else + { + throw new ArgumentException(SR.Get(SRID.RecognizerRuleNotFoundStream, ruleName), nameof(ruleName)); + } + } + else + { + if (string.IsNullOrEmpty(ruleName)) + { + throw new FormatException(SR.Get(SRID.RecognizerNoRootRuleToActivate1, uri)); + } + else + { + throw new ArgumentException(SR.Get(SRID.RecognizerRuleNotFound, ruleName, uri), nameof(ruleName)); + } + } + } + + // We can proceed if the audio is not found as this call could be for emulation. + else if (errorCode != SAPIErrorCodes.SPERR_AUDIO_NOT_FOUND && errorCode < 0) + { + ThrowIfSapiErrorCode(errorCode); + throw new COMException(SR.Get(SRID.RecognizerRuleActivationFailed), (int)errorCode); + } + } + + // Method called on background thread {from RecognizeAsync} to start recognition process. +#pragma warning disable 56500 // Transferring exceptions to another thread + + private void RecognizeAsyncWaitForGrammarsToLoad(object unused) + { + Debug.WriteLine("Waiting for any pending grammar loads to complete."); + // First we must wait until all pending grammars have loaded. + // Once we have the lock can release immediately - there's no need to hold on to it + _waitForGrammarsToLoad.WaitForOperationsToFinish(); + + Exception exception = null; // Keep track of any error we need to throw + bool cancelled = false; // If you call cancel while grammars are loading we don't bother starting recognition. + + lock (SapiRecognizer) + { + foreach (Grammar grammar in _grammars) + { + // Note all of the items called on Grammar are simple properties so we don't + // have any special locking even though this method could be called on different threads. + + if (grammar.State == GrammarState.LoadFailed) + { + // Note: For now there's no special exception for when RecognizeAsync fails because a grammar load failed. + // Instead just use whatever grammar exception was fired. + Debug.WriteLine("Problem loading grammars."); + exception = grammar.LoadException; + break; + } + } + + Debug.Assert(_isRecognizing); + + // The app may have called RecognizeAsyncCancel by no so abort at this point and don't bother starting SAPI. + if (_isRecognizeCancelled) + { + Debug.WriteLine("Recognition cancelled while waiting for grammars to load."); + cancelled = true; + } + } + + // Now start the recognizer there was no exception and we are not cancelled. + if (exception == null && !cancelled) + { + try + { + if (!_isEmulateRecognition) + { + SapiRecognizer.SetRecoState(SPRECOSTATE.SPRST_ACTIVE_ALWAYS); + Debug.WriteLine("Grammar loads completed, recognition started."); + } + } + catch (COMException comException) + { + Debug.WriteLine("Problem starting recognition - sapi exception."); + exception = ExceptionFromSapiStreamError((SAPIErrorCodes)comException.ErrorCode); + } + catch (Exception fatalException) + { + Debug.WriteLine("Problem starting recognition - unknown exception."); + exception = fatalException; + } + } + + // If either an exception or the cancellation has occurred then we need to throw the RecognizeCompleted right away. + // Otherwise it will be thrown later when SAPI sends the EndStream event. + if (exception != null || cancelled) + { + RecognizeCompletedEventArgs eventArgs = new(null, false, false, false, TimeSpan.Zero, exception, cancelled, null); + _asyncWorkerUI.PostOperation(new WaitCallback(RecognizeAsyncWaitForGrammarsToLoadFailed), eventArgs); + } + } +#pragma warning restore 56500 + + // Method called on app thread model used to fire the RecognizeCompelted event args if recognition stopped prematurely + private void RecognizeAsyncWaitForGrammarsToLoadFailed(object eventArgs) + { + Debug.WriteLine("Firing RecognizeCompleted because recognition didn't start as expected."); + Debug.Assert(eventArgs != null); + + lock (SapiRecognizer) // Lock to protect _isRecognizing and _isRecognizeCancelled + { + // Might have got here because recognition was cancelled so reset flags. + Debug.Assert(_isRecognizing); + _isRecognizeCancelled = false; + _isRecognizing = false; + } + + // Now raise RecognizeCompleted event. + EventHandler recognizeCompletedHandler = RecognizeCompleted; + if (recognizeCompletedHandler != null) + { + recognizeCompletedHandler(this, (RecognizeCompletedEventArgs)eventArgs); + } + } + + // This method will be called asynchronously + private void SignalHandlerThread(object ignored) + { + if (_asyncWorkerUI.AsyncMode == false) + { + _handlerWaitHandle.Set(); + } + } + + // Main handler of sapi events. This method will be called asynchronously + private void DispatchEvents(object eventData) + { + lock (_thisObjectLock) + { + SpeechEvent speechEvent = eventData as SpeechEvent; + if (!_disposed && eventData != null) + { + switch (speechEvent.EventId) + { + case SPEVENTENUM.SPEI_START_SR_STREAM: + ProcessStartStreamEvent(); + break; + + case SPEVENTENUM.SPEI_PHRASE_START: + ProcessPhraseStartEvent(speechEvent); + break; + + case SPEVENTENUM.SPEI_SR_BOOKMARK: + ProcessBookmarkEvent(speechEvent); + break; + + case SPEVENTENUM.SPEI_HYPOTHESIS: + ProcessHypothesisEvent(speechEvent); + break; + + case SPEVENTENUM.SPEI_FALSE_RECOGNITION: + case SPEVENTENUM.SPEI_RECOGNITION: + ProcessRecognitionEvent(speechEvent); + break; + + case SPEVENTENUM.SPEI_RECO_OTHER_CONTEXT: + ProcessRecoOtherContextEvent(); + break; + + case SPEVENTENUM.SPEI_END_SR_STREAM: + ProcessEndStreamEvent(speechEvent); + break; + + case SPEVENTENUM.SPEI_INTERFERENCE: + ProcessInterferenceEvent((uint)speechEvent.LParam); + break; + + case SPEVENTENUM.SPEI_SR_AUDIO_LEVEL: + ProcessAudioLevelEvent((int)speechEvent.WParam); + break; + } + } + } + } + + private void ProcessStartStreamEvent() + { + lock (SapiRecognizer) + { + _audioState = AudioState.Silence; + } + + // Fire events + FireAudioStateChangedEvent(_audioState); + FireStateChangedEvent(RecognizerState.Listening); + + // Set the initial silence timeout running. + // We wait until this event in case there was some error that prevented the recognition from starting. + + TimeSpan initialSilenceTimeout = InitialSilenceTimeout; // This gets the value in a thread-safe manner. + + // Add bookmark at desired InitialSilence Timeout + if (_recognizeMode == RecognizeMode.Single && initialSilenceTimeout != TimeSpan.Zero) + { + if (_supportsSapi53) + { + SapiContext.Bookmark(SPBOOKMARKOPTIONS.SPBO_TIME_UNITS | SPBOOKMARKOPTIONS.SPBO_PAUSE, + (ulong)initialSilenceTimeout.Ticks, new IntPtr((int)_initialSilenceBookmarkId)); + } + else + { + SapiContext.Bookmark(SPBOOKMARKOPTIONS.SPBO_PAUSE, + TimeSpanToStreamPosition(initialSilenceTimeout), new IntPtr((int)_initialSilenceBookmarkId)); + } + _detectingInitialSilenceTimeout = true; + } + } + + private void ProcessPhraseStartEvent(SpeechEvent speechEvent) + { + // A phrase start event should be followed by a Recognition or False Recognition event + _isWaitingForRecognition = true; + + lock (SapiRecognizer) + { + _audioState = AudioState.Speech; + } + FireAudioStateChangedEvent(_audioState); + + // Set the babble timeout running. + + // Cancel any InitialSilenceTimeout detection. + _detectingInitialSilenceTimeout = false; + + TimeSpan babbleTimeout = BabbleTimeout; // This gets the value in a thread-safe manner. + + // Add bookmark at BabbleTimeout + if (_recognizeMode == RecognizeMode.Single && babbleTimeout != TimeSpan.Zero) + { + // Don't make this a pausing bookmark or it will have to wait for the engine to reach a sync point ... + if (_supportsSapi53) + { + SapiContext.Bookmark(SPBOOKMARKOPTIONS.SPBO_TIME_UNITS, + (ulong)((babbleTimeout + speechEvent.AudioPosition).Ticks), new IntPtr((int)_babbleBookmarkId)); + } + else + { + SapiContext.Bookmark(SPBOOKMARKOPTIONS.SPBO_NONE, + TimeSpanToStreamPosition(babbleTimeout) + speechEvent.AudioStreamOffset, new IntPtr((int)_babbleBookmarkId)); + } + _detectingBabbleTimeout = true; + } + + // Fire the SpeechDetected event. + FireSpeechDetectedEvent(speechEvent.AudioPosition); + } + + private void ProcessBookmarkEvent(SpeechEvent speechEvent) + { + // A bookmark can either be triggered from a timeout, + // in which case the recognition process is stopped; + // or from a call to RequestRecognizerUpdate, in + // which case the RecognizerUpdateReached event is raised. + + uint bookmarkId = (uint)speechEvent.LParam; + + // We always call Resume even on error so have a try - finally block; + try + { + if (bookmarkId == _initialSilenceBookmarkId) + { + if (_detectingInitialSilenceTimeout) // If a phrase start has already happened we still get the bookmark but should ignore it. + { + EndRecognitionWithTimeout(); + } + } + else if (bookmarkId == _babbleBookmarkId) + { + // If a phrase start has already happened we still get the bookmark but should ignore it. + // Similarly don't ever fire both timeouts. + if (_detectingBabbleTimeout && !_initialSilenceTimeoutReached) + { + // Otherwise set the flag and cancel the recognition. + _babbleTimeoutReached = true; + SapiRecognizer.SetRecoState(SPRECOSTATE.SPRST_INACTIVE_WITH_PURGE); + } + } + else // Not a timeout so a real request to pause the engine + { + object userToken = GetBookmarkItemAndRemove(bookmarkId); + + EventHandler updateHandler = RecognizerUpdateReached; + if (updateHandler != null) + { + updateHandler(this, new RecognizerUpdateReachedEventArgs(userToken, speechEvent.AudioPosition)); + } + } + } + catch (COMException e) + { + throw ExceptionFromSapiCreateRecognizerError(e); + } + finally + { + // Always want to call Resume or we can hang the engine in the pause state. + // Currently all bookmarks pause but we check anyway for safety. + if (((SPRECOEVENTFLAGS)speechEvent.WParam & SPRECOEVENTFLAGS.SPREF_AutoPause) != 0) + { + SapiContext.Resume(); + } + } + } + + private void ProcessHypothesisEvent(SpeechEvent speechEvent) + { + RecognitionResult result = CreateRecognitionResult(speechEvent); + + bool enabled; + lock (SapiRecognizer) // Lock to protect _grammarEnabled + { + enabled = _enabled; + } + + // If the result corresponds to a real, active grammar (result.Grammar != null), + // And the Enabled property is set, + // then proceed and raise the event. + // Otherwise, the Grammar has been unloaded or deactivated so skip the event. + if (result.Grammar != null && result.Grammar.Enabled && enabled) + { + Debug.Assert(result.Grammar.State == GrammarState.Loaded); + + // Fire the hypothesis event. + FireSpeechHypothesizedEvent(result); + } + } + + private void ProcessRecognitionEvent(SpeechEvent speechEvent) + { + // First disable timeouts. + _detectingInitialSilenceTimeout = false; + _detectingBabbleTimeout = false; + bool isRecognizeCancelled = true; + bool isEmulate = (speechEvent.WParam & (ulong)SPRECOEVENTFLAGS.SPREF_Emulated) != 0; + + try + { + RecognitionResult result = CreateRecognitionResult(speechEvent); + + bool enabled; + lock (SapiRecognizer) // Lock to protect _grammarEnabled, _isRecognizeCancelled, and _audioStatus. + { + _audioState = AudioState.Silence; + isRecognizeCancelled = _isRecognizeCancelled; + enabled = _enabled; + } + + FireAudioStateChangedEvent(_audioState); + + // If the result corresponds to a real, active grammar (result.Grammar != null), + // Or the result corresponds to an event which belongs to no grammar (result.GrammarId == 0), + // And the Enabled property is set, + // then proceed and raise the event. + // Otherwise, the Grammar has been unloaded or deactivated so skip the event. + // Note: this doesn't absolutely guarantee an event won't be fired after the grammar is unloaded + // - there's a small window after this check is done and before the event fires where the grammar could get + // unloaded. To fix this would require more strict locking here. + if (((result.Grammar != null && result.Grammar.Enabled) || + (speechEvent.EventId == SPEVENTENUM.SPEI_FALSE_RECOGNITION && result.GrammarId == 0)) && + enabled) + { + if (speechEvent.EventId == SPEVENTENUM.SPEI_RECOGNITION) + { + // Remember the last result so we can fire it again in the RecognitionCompleted event. + // Note this is only done for Recognition, not for a rejected Recognition. + _lastResult = result; + + // Fire the recognition on the grammar. + SpeechRecognizedEventArgs recognitionEventArgs = new(result); + result.Grammar.OnRecognitionInternal(recognitionEventArgs); + + // Fire the recognition on the recognizer. + FireSpeechRecognizedEvent(recognitionEventArgs); + } + else + { + // Although we send a result in RecognitionRejected event, we would want a null + // result in RecognitionCompleted event. + _lastResult = null; + + // SPEVENTENUM.SPEI_FALSE_RECOGNITION + // Fire the event but if SAPI will fire an empty false recognition after each timeout + // or when the recognition has been shut off. Don't report these events since then don't contain useful info. + if (result.GrammarId != 0 || !(_babbleTimeoutReached || isRecognizeCancelled)) + { + // Fire the rejected recognition on the recognizer. + FireSpeechRecognitionRejectedEvent(result); + } + } + } + // else Grammar has already been unloaded or disabled - so don't fire result + + } + finally // Even if event handler throws we should call this + { + if (_recognizeMode == RecognizeMode.Single) + { + // Always stop recognizer after each recognition or false recognition in Automatic mode. + // - Same as RecognizeAsyncCancel but don't want to set _isRecognizeCancelled flag; + try + { + SapiRecognizer.SetRecoState(SPRECOSTATE.SPRST_INACTIVE_WITH_PURGE); + } + catch (COMException e) + { + throw ExceptionFromSapiCreateRecognizerError(e); + } + } + + if (((SPRECOEVENTFLAGS)speechEvent.WParam & SPRECOEVENTFLAGS.SPREF_AutoPause) != 0) + { + SapiContext.Resume(); + } + } + + // + // Set a flag so we will fire recognition completed event when we receive SR_END_STREAM. + // + // In the inproc case, we will not be able to do simultaneous recognition, so this is + // the recognition we are waiting for. + // In the shared case, we can do emulation during recognition, but we only wait for the + // emulate result. + // + if (_inproc || isEmulate) + { + _isWaitingForRecognition = false; + } + if (isEmulate && !_inproc) + { + // Fire the EmulateRecognizeCompleted event + FireEmulateRecognizeCompletedEvent(_lastResult, _lastException, isRecognizeCancelled); + } + } + + private void ProcessRecoOtherContextEvent() + { + if (_isEmulateRecognition && !_inproc) + { + // Fire the EmulateRecognizeCompleted event + FireEmulateRecognizeCompletedEvent(_lastResult, _lastException, false); + } + + lock (SapiRecognizer) + { + _audioState = AudioState.Silence; + } + FireAudioStateChangedEvent(_audioState); + } + + private void ProcessEndStreamEvent(SpeechEvent speechEvent) + { + // + // Emulation on SAPI5.1 can send bogus end stream events before a recognition + // + if (!_supportsSapi53 && _isEmulateRecognition && _isWaitingForRecognition) + { + return; + } + + // All queued bookmarks can be removed now. + // Don't reset with EmulatedResults - because you can Emulate during a recognition {in SpeechRecognizer}, + // this means multiple EndStreamEvents can be fired together which confuses the BookmarkTable clean-up. + if (((SPENDSRSTREAMFLAGS)speechEvent.WParam & SPENDSRSTREAMFLAGS.SPESF_EMULATED) == 0) + { + ResetBookmarkTable(); + } + + // Remember variables we need later. + bool initialSilenceTimeoutReached = _initialSilenceTimeoutReached; + bool babbleTimeoutReached = _babbleTimeoutReached; + + RecognitionResult lastResult = _lastResult; + Exception lastException = _lastException; + + // Reset all variables so you can restart recognition immediately (from within RecognizeCompleted event handler). + _initialSilenceTimeoutReached = false; + _babbleTimeoutReached = false; + _detectingInitialSilenceTimeout = false; + _detectingBabbleTimeout = false; + _lastResult = null; + _lastException = null; + + bool isStreamReleased = false; + bool isRecognizeCancelled; + lock (SapiRecognizer) // Lock to protect _isRecognizing, _isRecognizeCancelled, _haveInputSource, _audioFormat, _audioStatus. + { + _audioState = AudioState.Stopped; + + if (((SPENDSRSTREAMFLAGS)speechEvent.WParam & SPENDSRSTREAMFLAGS.SPESF_STREAM_RELEASED) == SPENDSRSTREAMFLAGS.SPESF_STREAM_RELEASED) + { + isStreamReleased = true; + _haveInputSource = false; + } + + isRecognizeCancelled = _isRecognizeCancelled; + + _isRecognizeCancelled = false; + _isRecognizing = false; + } + + Debug.Assert(!(initialSilenceTimeoutReached && babbleTimeoutReached)); // Both timeouts should not be set + FireAudioStateChangedEvent(_audioState); + + // Fire the RecognizeCompleted event. (Except in the emulation case) + if (!_isEmulateRecognition) + { + FireRecognizeCompletedEvent(lastResult, initialSilenceTimeoutReached, babbleTimeoutReached, isStreamReleased, speechEvent.AudioPosition, (speechEvent.LParam == 0) ? null : ExceptionFromSapiStreamError((SAPIErrorCodes)speechEvent.LParam), isRecognizeCancelled); + } + else + { + // + // followed by a recognition/false recognition event. But it is not the case at this point as we + // actually receive multiple SR_END_STREAM events for a single emulation, and the first SR_END_STREAM + // is not proceeded by a recognition event. Until we found the problem in SAPI, this is only a workaround + // + + // Fire the EmulateRecognizeCompleted event + // Don't reset with EmulatedResults - because you can Emulate during a recognition {in SpeechRecognizer}, + // this means multiple EndStreamEvents can be fired together which confuses the BookmarkTable clean-up. + + FireEmulateRecognizeCompletedEvent(lastResult, (speechEvent.LParam == 0) ? lastException : ExceptionFromSapiStreamError((SAPIErrorCodes)speechEvent.LParam), isRecognizeCancelled); + } + + // Fire state changed event + FireStateChangedEvent(RecognizerState.Stopped); + } + + private void ProcessInterferenceEvent(uint interference) + { + // Don't actually read the value here because we get it in a call to GetStatus later. + FireSignalProblemOccurredEvent((AudioSignalProblem)interference); + } + + private void ProcessAudioLevelEvent(int audioLevel) + { + // Don't actually read the value here because we get it in a call to GetStatus later. + FireAudioLevelUpdatedEvent(audioLevel); + } + + private void EndRecognitionWithTimeout() + { + _initialSilenceTimeoutReached = true; + + // Got a timeout so cancel Recognition. + // - Same as RecognizeAsyncCancel but don't want to set _isRecognizeCancelled flag; + + SapiRecognizer.SetRecoState(SPRECOSTATE.SPRST_INACTIVE_WITH_PURGE); + + // Note we don't directly raise a SpeechRecognitionRejected event in this scenario. + // However SAPI should always raise a FALSE_RECOGNITION after canceling. + } + + private RecognitionResult CreateRecognitionResult(SpeechEvent speechEvent) + { + // Get the sapi result + ISpRecoResult sapiResult = (ISpRecoResult)Marshal.GetObjectForIUnknown((IntPtr)speechEvent.LParam); + RecognitionResult recoResult = null; + + // Get the serialized unmanaged blob and then delete the sapi result + IntPtr coMemSerializeBlob; + sapiResult.Serialize(out coMemSerializeBlob); + byte[] serializedBlob = null; + + try + { + // Convert the unmanaged blob to managed and delete the unmanaged memory + uint sizeOfSerializedBlob = (uint)Marshal.ReadInt32(coMemSerializeBlob); + serializedBlob = new byte[sizeOfSerializedBlob]; + Marshal.Copy(coMemSerializeBlob, serializedBlob, 0, (int)sizeOfSerializedBlob); + } + finally + { + Marshal.FreeCoTaskMem(coMemSerializeBlob); + } + // Now create a RecognitionResult. + // For normal recognitions and false recognitions this will have all the information in it. + // For a false recognition with no phrase the result should still be valid, just empty. + recoResult = new RecognitionResult(this, sapiResult, serializedBlob, MaxAlternates); + + return recoResult; + } + + // Reset the AudioFormat property - needed when the format might have changed. + // Also update the EventNotify so it can calculate event AudioPositions from byte offsets correctly. + private void UpdateAudioFormat(SpeechAudioFormatInfo audioFormat) + { + lock (SapiRecognizer) // Lock to protect _audioFormat + { + // This code could be skipped for SAPI 5.3 - just reset _audioFormat and _eventNotify.AudioFormat to null. + // But for consistency do the same in both scenarios. + try + { + _audioFormat = GetSapiAudioFormat(); + } + catch (ArgumentException) + { + _audioFormat = audioFormat; + } + _eventNotify.AudioFormat = _audioFormat; // Update EventNotify so subsequent events get correct AudioPosition. + } + } + + // Calls through to Sapi to get the current engine audio format. + private SpeechAudioFormatInfo GetSapiAudioFormat() + { + IntPtr waveFormatPtr = IntPtr.Zero; + SpeechAudioFormatInfo formatInfo = null; + bool hasWaveFormat = false; + try + { + try + { + // Get the format for that engine + waveFormatPtr = SapiRecognizer.GetFormat(SPSTREAMFORMATTYPE.SPWF_SRENGINE); + if (waveFormatPtr != IntPtr.Zero) + { + if ((formatInfo = AudioFormatConverter.ToSpeechAudioFormatInfo(waveFormatPtr)) != null) + { + hasWaveFormat = true; + } + } + } + catch (COMException) + { + } + + // If for some reason the GetFormat fails OR we can't get a wave format, assume 16 Kb, 16 bits, Audio. + if (!hasWaveFormat) + { + formatInfo = new SpeechAudioFormatInfo(16000, AudioBitsPerSample.Sixteen, AudioChannel.Mono); + } + } + finally + { + if (waveFormatPtr != IntPtr.Zero) + { + Marshal.FreeCoTaskMem(waveFormatPtr); + } + } + return formatInfo; + } + + // Convert a TimeSpan such as initialSilenceTimeout to a byte offset using the + // current audio format. This should only needed if not using SAPI 5.3. + private ulong TimeSpanToStreamPosition(TimeSpan time) + { + return (ulong)(time.Ticks * AudioFormat.AverageBytesPerSecond) / TimeSpan.TicksPerSecond; + } + + // Converts COM errors returned by SPEI_END_SR_STREAM or SetRecoState to an appropriate .NET exception. + private static void ThrowIfSapiErrorCode(SAPIErrorCodes errorCode) + { + SRID srid = SapiConstants.SapiErrorCode2SRID(errorCode); + if ((int)srid >= 0) + { + throw new InvalidOperationException(SR.Get(srid)); + } + } + + // Converts COM errors returned by SPEI_END_SR_STREAM or SetRecoState to an appropriate .NET exception. + private static Exception ExceptionFromSapiStreamError(SAPIErrorCodes errorCode) + { + SRID srid = SapiConstants.SapiErrorCode2SRID(errorCode); + if ((int)srid >= 0) + { + return new InvalidOperationException(SR.Get(srid)); + } + else + { + return new COMException(SR.Get(SRID.AudioDeviceInternalError), (int)errorCode); + } + } + + // Convert the .NET CompareOptions into the SAPI SpeechEmulationCompareFlags. + private static SpeechEmulationCompareFlags ConvertCompareOptions(CompareOptions compareOptions) + { + CompareOptions handledOptions = CompareOptions.IgnoreCase | CompareOptions.OrdinalIgnoreCase | CompareOptions.IgnoreKanaType | CompareOptions.IgnoreWidth | CompareOptions.Ordinal; + SpeechEmulationCompareFlags flags = 0; + if ((compareOptions & CompareOptions.IgnoreCase) != 0 || (compareOptions & CompareOptions.OrdinalIgnoreCase) != 0) + { + flags |= SpeechEmulationCompareFlags.SECFIgnoreCase; + } + if ((compareOptions & CompareOptions.IgnoreKanaType) != 0) + { + flags |= SpeechEmulationCompareFlags.SECFIgnoreKanaType; + } + if ((compareOptions & CompareOptions.IgnoreWidth) != 0) + { + flags |= SpeechEmulationCompareFlags.SECFIgnoreWidth; + } + if ((compareOptions & ~handledOptions) != 0) + { + throw new NotSupportedException(SR.Get(SRID.NotSupportedWithThisVersionOfSAPICompareOption)); + } + return flags; + } + + // Methods to add and remove SAPI event interests. + + internal void AddEventInterest(ulong interest) + { + if ((_eventInterest & interest) != interest) + { + _eventInterest |= interest; + SapiContext.SetInterest(_eventInterest, _eventInterest); + } + } + + internal void RemoveEventInterest(ulong interest) + { + if ((_eventInterest & interest) != 0) + { + _eventInterest &= ~interest; + SapiContext.SetInterest(_eventInterest, _eventInterest); + } + } + + // Bookmark related methods: + // A dictionary is used to map between userToken objects supplied to the RequestRecognizerUpdate event + // and the sapi bookmark lparam value. + // The uint key from the dictionary is stored in the sapi event and used to recover the userToken reference. + // The following methods encapsulate this functionality. + // The bookmarks used for the InitialSilenceTimeout and BabbleTimeout are also stored in this table. + // To prevent the dictionary growing too much, each bookmark event removes itself from the hashtable, the end stream event clears the table. + + private uint AddBookmarkItem(object userToken) + { + uint bookmarkId = 0; + if (userToken != null) // Null item always maps to zero id. + { + lock (_bookmarkTable) // Lock to protect _nextBookmarkId and _bookmarkTable + { + bookmarkId = _nextBookmarkId++; // Find the next bookmark id to use. + + if (_nextBookmarkId == 0) + { + // As long as there are not 2^32 outstanding bookmarks this will work fine. + // There's also a case where the bookmark table doesn't completely reset + // during ResetBookmarkTable but this would require 2^32 bookmarks also. + throw new InvalidOperationException(SR.Get(SRID.RecognizerUpdateTableTooLarge)); + } + + _bookmarkTable[unchecked((int)bookmarkId)] = userToken; + Debug.WriteLine("Added bookmark: " + bookmarkId + " " + userToken); + } + } + return bookmarkId; + } + + private void ResetBookmarkTable() + { + lock (_bookmarkTable) // Lock to protect _nextBookmarkId and _bookmarkTable + { + // Don't delete every bookmark, because there's an edge case where a bookmark, + // can be requested just before the EndStream event and be fired just after. + // So only clear the table up to the max value from the PREVIOUS recognition. + + // There's no way to enumerate through the Dictionary while deleting some keys. + // So make a copy of the keys first. + if (_bookmarkTable.Count > 0) + { + int[] keysArray = new int[_bookmarkTable.Count]; + _bookmarkTable.Keys.CopyTo(keysArray, 0); + for (int i = 0; i < keysArray.Length; i++) + { + if (keysArray[i] <= _prevMaxBookmarkId) + { + _bookmarkTable.Remove(keysArray[i]); + } + } + } + + if (_bookmarkTable.Count == 0) + { + // Now reset the _nextBookmarkId. + // Remember that several values are predefined and must not be used, so reset to _intialBookmarkId + _nextBookmarkId = _firstUnusedBookmarkId; + _prevMaxBookmarkId = _firstUnusedBookmarkId - 1; + } + else + { + // If there's still bookmarks in the table that might still fire, + // then update _prevMaxBookmarkId. At the end of the next recognition they will be deleted. + _prevMaxBookmarkId = _nextBookmarkId - 1; + } + //Debug.WriteLine ("Reset bookmarks: count=" + _bookmarkTable.Count + " max=" + _prevMaxBookmarkId + " next=" + _nextBookmarkId); + } + } + + private object GetBookmarkItemAndRemove(uint bookmarkId) + { + object userToken = null; + if (bookmarkId != 0) // Zero is a special case where the lookup table is bypassed. + { + lock (_bookmarkTable) // Lock to protect _bookmarkTable + { + int id = unchecked((int)bookmarkId); + userToken = _bookmarkTable[id]; + _bookmarkTable.Remove(id); + Debug.WriteLine("Fired bookmark: " + bookmarkId + " " + userToken); + } + } + return userToken; + } + + private void CloseCachedInputStream() + { + if (_inputStream != null) + { + _inputStream.Close(); + _inputStream = null; + } + } + + /// + /// Fire audio status changed event + /// + private void FireAudioStateChangedEvent(AudioState audioState) + { + EventHandler audioStateChangedHandler = _audioStateChangedDelegate; + if (audioStateChangedHandler != null) + { + _asyncWorkerUI.PostOperation(audioStateChangedHandler, this, new AudioStateChangedEventArgs(audioState)); + } + } + + /// + /// Fire audio status changed event + /// + private void FireSignalProblemOccurredEvent(AudioSignalProblem audioSignalProblem) + { + EventHandler audioSignalProblemOccuredHandler = _audioSignalProblemOccurredDelegate; + if (audioSignalProblemOccuredHandler != null) + { + TimeSpan recognizerPosition = TimeSpan.Zero; + TimeSpan audioPosition = TimeSpan.Zero; + + try + { + // These calls do not wait for engine sync point so should be fast. + SPRECOGNIZERSTATUS recoStatus; + recoStatus = SapiRecognizer.GetStatus(); + + lock (SapiRecognizer) // Lock to protect _audioStatus. + { + SpeechAudioFormatInfo audioFormat = AudioFormat; + audioPosition = audioFormat.AverageBytesPerSecond > 0 ? new TimeSpan((long)((recoStatus.AudioStatus.CurDevicePos * TimeSpan.TicksPerSecond) / (ulong)audioFormat.AverageBytesPerSecond)) : TimeSpan.Zero; + recognizerPosition = new TimeSpan((long)recoStatus.ullRecognitionStreamTime); + } + } + catch (COMException e) + { + throw ExceptionFromSapiCreateRecognizerError(e); + } + + _asyncWorkerUI.PostOperation(audioSignalProblemOccuredHandler, this, new AudioSignalProblemOccurredEventArgs(audioSignalProblem, AudioLevel, audioPosition, recognizerPosition)); + } + } + + /// + /// Fire audio status changed event + /// + private void FireAudioLevelUpdatedEvent(int audioLevel) + { + EventHandler audioLevelUpdatedHandler = _audioLevelUpdatedDelegate; + if (audioLevelUpdatedHandler != null) + { + _asyncWorkerUI.PostOperation(audioLevelUpdatedHandler, this, new AudioLevelUpdatedEventArgs(audioLevel)); + } + } + + private void FireStateChangedEvent(RecognizerState recognizerState) + { + // Fire state changed event + EventHandler stateChangedHandler = StateChanged; + if (stateChangedHandler != null) + { + _asyncWorkerUI.PostOperation(stateChangedHandler, this, new StateChangedEventArgs(recognizerState)); + } + } + /// + /// Fire the SpeechDetected event. + /// + private void FireSpeechDetectedEvent(TimeSpan audioPosition) + { + EventHandler speechDetectedHandler = SpeechDetected; + if (speechDetectedHandler != null) + { + _asyncWorkerUI.PostOperation(speechDetectedHandler, this, new SpeechDetectedEventArgs(audioPosition)); + } + } + + /// + /// Fire the hypothesis event. + /// + private void FireSpeechHypothesizedEvent(RecognitionResult result) + { + EventHandler speechHypothesizedHandler = _speechHypothesizedDelegate; + if (speechHypothesizedHandler != null) + { + _asyncWorkerUI.PostOperation(speechHypothesizedHandler, this, new SpeechHypothesizedEventArgs(result)); + } + } + + /// + /// Fire the rejected recognition on the recognizer. + /// + private void FireSpeechRecognitionRejectedEvent(RecognitionResult result) + { + EventHandler recognitionHandler = SpeechRecognitionRejected; + SpeechRecognitionRejectedEventArgs recognitionEventArgs = new(result); + if (recognitionHandler != null) + { + _asyncWorkerUI.PostOperation(recognitionHandler, this, recognitionEventArgs); + } + } + + /// + /// Fire the recognition on the grammar. + /// + private void FireSpeechRecognizedEvent(SpeechRecognizedEventArgs recognitionEventArgs) + { + EventHandler recognitionHandler = SpeechRecognized; + if (recognitionHandler != null) + { + _asyncWorkerUI.PostOperation(recognitionHandler, this, recognitionEventArgs); + } + } + + /// + /// Fire the recognition completed event. + /// + private void FireRecognizeCompletedEvent(RecognitionResult result, bool initialSilenceTimeoutReached, bool babbleTimeoutReached, bool isStreamReleased, TimeSpan audioPosition, Exception exception, bool isRecognizeCancelled) + { + // In the synchronous case, fire the private event + EventHandler recognizeCompletedHandler = RecognizeCompletedSync; + if (recognizeCompletedHandler == null) + { + // If not in sync mode, fire the public event. + recognizeCompletedHandler = RecognizeCompleted; + } + + // Fire the completed event + if (recognizeCompletedHandler != null) + { + _asyncWorkerUI.PostOperation(recognizeCompletedHandler, this, new RecognizeCompletedEventArgs(result, initialSilenceTimeoutReached, babbleTimeoutReached, + isStreamReleased, audioPosition, exception, isRecognizeCancelled, null)); + } + } + + /// + /// Fire the emulate completed event. + /// + private void FireEmulateRecognizeCompletedEvent(RecognitionResult result, Exception exception, bool isRecognizeCancelled) + { + EventHandler emulateRecognizeCompletedHandler; + lock (SapiRecognizer) + { + // In the synchronous case, fire the private event + emulateRecognizeCompletedHandler = EmulateRecognizeCompletedSync; + if (emulateRecognizeCompletedHandler == null) + { + // If not in sync mode, fire the public event. + emulateRecognizeCompletedHandler = EmulateRecognizeCompleted; + } + _lastResult = null; + _lastException = null; + _isEmulateRecognition = false; + _isRecognizing = false; + + _isWaitingForRecognition = false; + } + + if (emulateRecognizeCompletedHandler != null) + { + _asyncWorkerUI.PostOperation(emulateRecognizeCompletedHandler, this, new EmulateRecognizeCompletedEventArgs(result, exception, isRecognizeCancelled, null)); + } + } + + private static void CheckGrammarOptionsOnSapi51(Grammar grammar) + { + SRID messageId = (SRID)(-1); + if (grammar.BaseUri != null && !grammar.IsSrgsDocument) + { + messageId = SRID.NotSupportedWithThisVersionOfSAPIBaseUri; + } + else if (grammar.IsStg || grammar.Sapi53Only) + { + messageId = SRID.NotSupportedWithThisVersionOfSAPITagFormat; + } + if (messageId != (SRID)(-1)) + { + throw new NotSupportedException(SR.Get(messageId)); + } + } + + #endregion + + #region Private Fields + + private List _grammars; + private ReadOnlyCollection _readOnlyGrammars; + + private RecognizerInfo _recognizerInfo; + private bool _disposed; + + // Internal Id incremented and passed to SAPI each time a grammar is created + private ulong _currentGrammarId; + + // Associated sapi interfaces + private SapiRecoContext _sapiContext; + private SapiRecognizer _sapiRecognizer; + private bool _supportsSapi53; + + private EventNotify _eventNotify; + private ulong _eventInterest; + + private EventHandler _audioSignalProblemOccurredDelegate; + private EventHandler _audioLevelUpdatedDelegate; + private EventHandler _audioStateChangedDelegate; + private EventHandler _speechHypothesizedDelegate; + + private bool _enabled = true; // Used by SpeechRecognizer to globally deactivate grammars. + + private int _maxAlternates; + internal AudioState _audioState; + private SpeechAudioFormatInfo _audioFormat; + + private RecognizeMode _recognizeMode = RecognizeMode.Multiple; // Default for SpeechRecognizer, set explicitly on SpeechRecognitionEngine + private bool _isRecognizeCancelled; + private bool _isRecognizing; + private bool _isEmulateRecognition; // The end of stream event is not fire on error for emulate recognition in SAPI 5.1 + private bool _isWaitingForRecognition; + + private RecognitionResult _lastResult; // Temporarily store last result but always set to null once recognition completes + private Exception _lastException; // Temporarily store last exception but always set to null once recognition completes + + // Means that the recognizer will be paused after each recognition while the SpeechRecognized event is firing. + // This is always on for the SpeechRecognitionEngine but off by default for the SpeechRecognizer. + private bool _pauseRecognizerOnRecognition = true; + + private bool _detectingInitialSilenceTimeout; + private bool _detectingBabbleTimeout; + private bool _initialSilenceTimeoutReached; + private bool _babbleTimeoutReached; + private TimeSpan _initialSilenceTimeout; + private TimeSpan _babbleTimeout; + + internal bool _haveInputSource; // Tracks if there's an input stream set or not - only used on SpeechRecognitionEngine. + private Stream _inputStream; // track the input stream open if it has been opened by this object + + // Dictionary used to map between sapi bookmark ids and RequestRecognizerUpdate userToken values. + private Dictionary _bookmarkTable = new(); + private uint _nextBookmarkId = _firstUnusedBookmarkId; + private uint _prevMaxBookmarkId = _firstUnusedBookmarkId - 1; + + // Lock used to wait for all pending async grammar loads to complete before starting recognition. + private OperationLock _waitForGrammarsToLoad = new(); + // Lock used to protect properties on the Grammar {Enabled, Weight etc.} from being changed while an async grammar load is in progress. + private object _grammarDataLock = new(); + + // Preset bookmark values. + private const uint _nullBookmarkId = 0; + private const uint _initialSilenceBookmarkId = _nullBookmarkId + 1; // 1 + private const uint _babbleBookmarkId = _initialSilenceBookmarkId + 1; // 2 + private const uint _firstUnusedBookmarkId = _babbleBookmarkId + 1; // 3 + + private AsyncSerializedWorker _asyncWorker, _asyncWorkerUI; + private AutoResetEvent _handlerWaitHandle = new(false); + + private object _thisObjectLock = new(); + + private Exception _loadException; + private Grammar _topLevel; + + private bool _inproc; + + // private event used to hook up the SpeechRecognitionEngine RecognizeCompleted event. + private event EventHandler RecognizeCompletedSync; + private event EventHandler EmulateRecognizeCompletedSync; + + private TimeSpan _defaultTimeout = TimeSpan.FromSeconds(30); + + private RecognizerBaseThunk _recoThunk; + #endregion + + private class RecognizerBaseThunk : ISpGrammarResourceLoader + { + internal RecognizerBaseThunk(RecognizerBase recognizer) + { + _recognizerRef = new WeakReference(recognizer); + } + + internal RecognizerBase Recognizer + { + get + { + return (RecognizerBase)_recognizerRef.Target; + } + } + + /// + /// Called to load a grammar and all of its dependent rule refs. + /// + /// Returns the CFG data for a given file and builds a tree of rule ref dependencies. + /// + int ISpGrammarResourceLoader.LoadResource(string bstrResourceUri, bool fAlwaysReload, out IStream pStream, ref string pbstrMIMEType, ref short pfModified, ref string pbstrRedirectUrl) + { + return ((ISpGrammarResourceLoader)Recognizer).LoadResource(bstrResourceUri, fAlwaysReload, out pStream, ref pbstrMIMEType, ref pfModified, ref pbstrRedirectUrl); + } + + /// + /// Unused + /// + string ISpGrammarResourceLoader.GetLocalCopy(Uri resourcePath, out string mimeType, out Uri redirectUrl) + { + return ((ISpGrammarResourceLoader)Recognizer).GetLocalCopy(resourcePath, out mimeType, out redirectUrl); + } + + /// + /// Unused + /// + void ISpGrammarResourceLoader.ReleaseLocalCopy(string path) + { + ((ISpGrammarResourceLoader)Recognizer).ReleaseLocalCopy(path); + } + + private WeakReference _recognizerRef; + } + } + + // Internal class used to encapsulate all the additional data the RecognizerBase needs about a Grammar. + // This is stored in the Grammar.InternalData property. + internal class InternalGrammarData + { + #region Constructors + + // Keep a copy of enabled, weight and priority because there's a race condition between reading the values from the Grammar + // to initially call SetSapiGrammarProperties and an app setting a property on the Grammar at the same time. + // Thus these copied values are taken under a lock and used to update sapi. + // This is to avoid having a lock which spans both the Grammar and Recognizer which would be awkward. + internal InternalGrammarData(ulong grammarId, SapiGrammar sapiGrammar, bool enabled, float weight, int priority) + { + _grammarId = grammarId; + _sapiGrammar = sapiGrammar; + _grammarEnabled = enabled; + _grammarWeight = weight; + _grammarPriority = priority; + } + + #endregion + + #region Internal Fields + + internal ulong _grammarId; // Id passed to SAPI's CreateGrammar call and returned in result. + internal SapiGrammar _sapiGrammar; + internal bool _grammarEnabled; + internal float _grammarWeight; + internal int _grammarPriority; + + #endregion + } + + // Simple class that keeps track of multiple threads performing an operation, and then allows another thread + // to wait until all operations have completed. This is similar in concept to a ReaderWriterLock, except + // in the ReaderWriterLock all Acquire/Releases must be on the same threads, where here StartOperation and FinishOperation + // can be on different threads. + // This is used in async grammar loading - all LoadGrammarAsync starts an activity, and then later they finished + // (on a different thread). WaitForOperationsToFinish is called by RecognizeAsync to wait for all loads to finish + // before starting recognition. + internal class OperationLock : IDisposable + { + public void Dispose() + { + _event.Close(); + GC.SuppressFinalize(this); + } + + internal void StartOperation() + { + lock (_thisObjectLock) // Not a publicly exposed class so okay to lock. + { + if (_operationCount == 0) + { + _event.Reset(); // Activities in progress so start blocking the WaitForActivitiesToFinish method. + } + _operationCount++; + } + } + + internal void FinishOperation() + { + lock (_thisObjectLock) + { + _operationCount--; + if (_operationCount == 0) + { + _event.Set(); // No more activities in progress so signal event. + } + } + } + + internal void WaitForOperationsToFinish() + { + _event.WaitOne(); + } + + private ManualResetEvent _event = new(true); // In signaled state so initially do not block + private uint _operationCount; + private object _thisObjectLock = new(); + } + + #region Interface + + [ComImport, Guid("2D3D3845-39AF-4850-BBF9-40B49780011D"), InterfaceType(ComInterfaceType.InterfaceIsIUnknown)] + internal interface ISpObjectTokenCategory : ISpDataKey + { + // ISpDataKey Methods + [PreserveSig] + new int SetData([MarshalAs(UnmanagedType.LPWStr)] string valueName, uint cbData, [MarshalAs(UnmanagedType.LPArray, SizeParamIndex = 1)] byte[] data); + [PreserveSig] + new int GetData([MarshalAs(UnmanagedType.LPWStr)] string valueName, ref uint pcbData, [MarshalAs(UnmanagedType.LPArray, SizeParamIndex = 1), Out] byte[] data); + [PreserveSig] + new int SetStringValue([MarshalAs(UnmanagedType.LPWStr)] string valueName, [MarshalAs(UnmanagedType.LPWStr)] string value); + [PreserveSig] + new void GetStringValue([MarshalAs(UnmanagedType.LPWStr)] string pszValueName, [MarshalAs(UnmanagedType.LPWStr)] out string ppszValue); + [PreserveSig] + new int SetDWORD([MarshalAs(UnmanagedType.LPWStr)] string valueName, uint dwValue); + [PreserveSig] + new int GetDWORD([MarshalAs(UnmanagedType.LPWStr)] string pszValueName, ref uint pdwValue); + [PreserveSig] + new int OpenKey([MarshalAs(UnmanagedType.LPWStr)] string pszSubKeyName, out ISpDataKey ppSubKey); + [PreserveSig] + new int CreateKey([MarshalAs(UnmanagedType.LPWStr)] string subKey, out ISpDataKey ppSubKey); + [PreserveSig] + new int DeleteKey([MarshalAs(UnmanagedType.LPWStr)] string subKey); + [PreserveSig] + new int DeleteValue([MarshalAs(UnmanagedType.LPWStr)] string valueName); + [PreserveSig] + new int EnumKeys(uint index, [MarshalAs(UnmanagedType.LPWStr)] out string ppszSubKeyName); + [PreserveSig] + new int EnumValues(uint Index, [MarshalAs(UnmanagedType.LPWStr)] out string ppszValueName); + + // ISpObjectTokenCategory Methods + void SetId([MarshalAs(UnmanagedType.LPWStr)] string pszCategoryId, [MarshalAs(UnmanagedType.Bool)] bool fCreateIfNotExist); + void GetId([MarshalAs(UnmanagedType.LPWStr)] out string ppszCoMemCategoryId); + void Slot14(); // void GetDataKey(System.Speech.Internal.SPDATAKEYLOCATION spdkl, out ISpDataKey ppDataKey); + void EnumTokens([MarshalAs(UnmanagedType.LPWStr)] string pzsReqAttribs, [MarshalAs(UnmanagedType.LPWStr)] string pszOptAttribs, out IEnumSpObjectTokens ppEnum); + void Slot16(); // void SetDefaultTokenId([MarshalAs(UnmanagedType.LPWStr)] string pszTokenId); + void GetDefaultTokenId([MarshalAs(UnmanagedType.LPWStr)] out string ppszCoMemTokenId); + } + + [ComImport, Guid("06B64F9E-7FDA-11D2-B4F2-00C04F797396"), InterfaceType(ComInterfaceType.InterfaceIsIUnknown)] + internal interface IEnumSpObjectTokens + { + void Slot1(); // void Next(UInt32 celt, [MarshalAs(UnmanagedType.LPArray, SizeParamIndex = 0), Out] ISpObjectToken[] pelt, out UInt32 pceltFetched); + void Slot2(); // void Skip(UInt32 celt); + void Slot3(); // void Reset(); + void Slot4(); // void Clone(out IEnumSpObjectTokens ppEnum); + void Item(uint Index, out ISpObjectToken ppToken); + void GetCount(out uint pCount); + } + + [ComImport, Guid("EF411752-3736-4CB4-9C8C-8EF4CCB58EFE")] + internal class SpObjectToken { } + + [ComImport, Guid("A910187F-0C7A-45AC-92CC-59EDAFB77B53")] + internal class SpObjectTokenCategory { } + + #endregion +} diff --git a/src/libraries/System.Speech/src/Recognition/RecognizerInfo.cs b/src/libraries/System.Speech/src/Recognition/RecognizerInfo.cs new file mode 100644 index 00000000000000..de1c0181da8e99 --- /dev/null +++ b/src/libraries/System.Speech/src/Recognition/RecognizerInfo.cs @@ -0,0 +1,156 @@ +// 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.Globalization; +using System.Speech.AudioFormat; +using System.Speech.Internal; +using System.Speech.Internal.ObjectTokens; + +namespace System.Speech.Recognition +{ + // This represents the attributes various speech recognizers may, or may not support. + + public class RecognizerInfo : IDisposable + { + #region Constructors + + private RecognizerInfo(ObjectToken token, CultureInfo culture) + { + // Retrieve the token name + _id = token.Name; + + // Retrieve default display name + _description = token.Description; + + // Store full object token id for internal use. + // NOTE - SAPI returns the wrong hive for tokenenum tokens (always HKLM). + // Do not rely on the path to be correct in all cases. + _sapiObjectTokenId = token.Id; + + _name = token.TokenName(); + + _culture = culture; + + // Enum all values and add to custom table + Dictionary attrs = new(); + foreach (string keyName in token.Attributes.GetValueNames()) + { + string attributeValue; + if (token.Attributes.TryGetString(keyName, out attributeValue)) + { + attrs[keyName] = attributeValue; + } + } + _attributes = new ReadOnlyDictionary(attrs); + + string audioFormats; + if (token.Attributes.TryGetString("AudioFormats", out audioFormats)) + { + _supportedAudioFormats = new ReadOnlyCollection(SapiAttributeParser.GetAudioFormatsFromString(audioFormats)); + } + else + { + _supportedAudioFormats = new ReadOnlyCollection(new List()); + } + + _objectToken = token; + } + + internal static RecognizerInfo Create(ObjectToken token) + { + // Token for recognizer should have Attributes. + if (token.Attributes == null) + { + return null; + } + + // Get other attributes + string langId; + + // must have a language id + if (!token.Attributes.TryGetString("Language", out langId)) + { + return null; + } + CultureInfo cultureInfo = SapiAttributeParser.GetCultureInfoFromLanguageString(langId); + if (cultureInfo != null) + { + return new RecognizerInfo(token, cultureInfo); + } + else + { + return null; + } + } + + internal ObjectToken GetObjectToken() + { + return _objectToken; + } + + /// + /// For IDisposable. + /// RecognizerInfo can be constructed through creating a new object token (usage of _recognizerInfo in RecognizerBase), + /// so dispose needs to be called. + /// + public void Dispose() + { + _objectToken.Dispose(); + GC.SuppressFinalize(this); + } + + #endregion + + #region public Properties + public string Id + { + get { return _id; } + } + public string Name + { + get { return _name; } + } + public string Description + { + get { return _description; } + } + public CultureInfo Culture + { + get { return _culture; } + } + public ReadOnlyCollection SupportedAudioFormats + { + get { return _supportedAudioFormats; } + } + public IDictionary AdditionalInfo + { + get { return _attributes; } + } + + #endregion + + #region Internal Properties + + #endregion + + #region Private Fields + + // This table stores each attribute + private ReadOnlyDictionary _attributes; + + // Named attributes - these get initialized in constructor + private string _id; + private string _name; + private string _description; + private string _sapiObjectTokenId; + private CultureInfo _culture; + + private ReadOnlyCollection _supportedAudioFormats; + + private ObjectToken _objectToken; + + #endregion + } +} diff --git a/src/libraries/System.Speech/src/Recognition/RecognizerState.cs b/src/libraries/System.Speech/src/Recognition/RecognizerState.cs new file mode 100644 index 00000000000000..f83c49087d1ba4 --- /dev/null +++ b/src/libraries/System.Speech/src/Recognition/RecognizerState.cs @@ -0,0 +1,15 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace System.Speech.Recognition +{ + // Current recognizer state. + public enum RecognizerState + { + // The recognizer is currently stopped and not listening. + Stopped, + + // The recognizer is currently listening. + Listening + } +} diff --git a/src/libraries/System.Speech/src/Recognition/RecognizerStateChangedEventArgs.cs b/src/libraries/System.Speech/src/Recognition/RecognizerStateChangedEventArgs.cs new file mode 100644 index 00000000000000..d3269b4dd7a4cb --- /dev/null +++ b/src/libraries/System.Speech/src/Recognition/RecognizerStateChangedEventArgs.cs @@ -0,0 +1,33 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace System.Speech.Recognition +{ + // EventArgs used in the SpeechRecognizer.StateChanged event. + + public class StateChangedEventArgs : EventArgs + { + #region Constructors + + internal StateChangedEventArgs(RecognizerState recognizerState) + { + _recognizerState = recognizerState; + } + + #endregion + + #region public Properties + public RecognizerState RecognizerState + { + get { return _recognizerState; } + } + + #endregion + + #region Private Fields + + private RecognizerState _recognizerState; + + #endregion + } +} diff --git a/src/libraries/System.Speech/src/Recognition/SemanticResultKey.cs b/src/libraries/System.Speech/src/Recognition/SemanticResultKey.cs new file mode 100644 index 00000000000000..34b6466d1c3839 --- /dev/null +++ b/src/libraries/System.Speech/src/Recognition/SemanticResultKey.cs @@ -0,0 +1,77 @@ +// 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; +using System.Speech.Internal; +using System.Speech.Internal.GrammarBuilding; + +namespace System.Speech.Recognition +{ + [DebuggerDisplay("{_semanticKey.DebugSummary}")] + public class SemanticResultKey + { + #region Constructors + + private SemanticResultKey(string semanticResultKey) + : base() + { + Helpers.ThrowIfEmptyOrNull(semanticResultKey, nameof(semanticResultKey)); + + _semanticKey = new SemanticKeyElement(semanticResultKey); + } + + public SemanticResultKey(string semanticResultKey, params string[] phrases) + : this(semanticResultKey) + { + Helpers.ThrowIfEmptyOrNull(semanticResultKey, nameof(semanticResultKey)); + Helpers.ThrowIfNull(phrases, nameof(phrases)); + + // Build a grammar builder with all the phrases + foreach (string phrase in phrases) + { + _semanticKey.Add(phrase); + } + } + + public SemanticResultKey(string semanticResultKey, params GrammarBuilder[] builders) + : this(semanticResultKey) + { + Helpers.ThrowIfEmptyOrNull(semanticResultKey, nameof(semanticResultKey)); + Helpers.ThrowIfNull(builders, "phrases"); + + // Build a grammar builder with all the grammar builders + foreach (GrammarBuilder builder in builders) + { + _semanticKey.Add(builder.Clone()); + } + } + + #endregion + + #region Public Methods + public GrammarBuilder ToGrammarBuilder() + { + return new GrammarBuilder(this); + } + + #endregion + + #region Internal Properties + + internal SemanticKeyElement SemanticKeyElement + { + get + { + return _semanticKey; + } + } + + #endregion + + #region Private Fields + + private readonly SemanticKeyElement _semanticKey; + + #endregion + } +} diff --git a/src/libraries/System.Speech/src/Recognition/SemanticResultValue.cs b/src/libraries/System.Speech/src/Recognition/SemanticResultValue.cs new file mode 100644 index 00000000000000..eb0a597f31b66e --- /dev/null +++ b/src/libraries/System.Speech/src/Recognition/SemanticResultValue.cs @@ -0,0 +1,63 @@ +// 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; +using System.Speech.Internal; +using System.Speech.Internal.GrammarBuilding; + +namespace System.Speech.Recognition +{ + [DebuggerDisplay("{_tag.DebugSummary}")] + public class SemanticResultValue + { + #region Constructors + public SemanticResultValue(object value) + { + Helpers.ThrowIfNull(value, nameof(value)); + + _tag = new TagElement(value); + } + public SemanticResultValue(string phrase, object value) + { + Helpers.ThrowIfEmptyOrNull(phrase, nameof(phrase)); + Helpers.ThrowIfNull(value, nameof(value)); + + _tag = new TagElement(new GrammarBuilderPhrase(phrase), value); + } + public SemanticResultValue(GrammarBuilder builder, object value) + { + Helpers.ThrowIfNull(builder, nameof(builder)); + Helpers.ThrowIfNull(value, nameof(value)); + + _tag = new TagElement(builder.Clone(), value); + } + + #endregion + + #region Public Methods + public GrammarBuilder ToGrammarBuilder() + { + return new GrammarBuilder(this); + } + + #endregion + + #region Internal Properties + + internal TagElement Tag + { + get + { + return _tag; + } + } + + #endregion + + #region Private Fields + + private TagElement _tag; + + #endregion + } +} diff --git a/src/libraries/System.Speech/src/Recognition/SpeechDetectedEventArgs.cs b/src/libraries/System.Speech/src/Recognition/SpeechDetectedEventArgs.cs new file mode 100644 index 00000000000000..e048b4a8e7d9cc --- /dev/null +++ b/src/libraries/System.Speech/src/Recognition/SpeechDetectedEventArgs.cs @@ -0,0 +1,33 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace System.Speech.Recognition +{ + // EventArgs used in the SpeechDetected event. + + public class SpeechDetectedEventArgs : EventArgs + { + #region Constructors + + internal SpeechDetectedEventArgs(TimeSpan audioPosition) + { + _audioPosition = audioPosition; + } + + #endregion + + #region public Properties + public TimeSpan AudioPosition + { + get { return _audioPosition; } + } + + #endregion + + #region Private Fields + + private TimeSpan _audioPosition; + + #endregion + } +} diff --git a/src/libraries/System.Speech/src/Recognition/SpeechRecognitionEngine.cs b/src/libraries/System.Speech/src/Recognition/SpeechRecognitionEngine.cs new file mode 100644 index 00000000000000..e13b59a5e2c6cc --- /dev/null +++ b/src/libraries/System.Speech/src/Recognition/SpeechRecognitionEngine.cs @@ -0,0 +1,690 @@ +// 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.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) + { + Helpers.ThrowIfNull(culture, nameof(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) + { + Helpers.ThrowIfNull(recognizerInfo, nameof(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 InstalledRecognizers() + { + List 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)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(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 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) + { + Helpers.ThrowIfNull(audioSource, nameof(audioSource)); + Helpers.ThrowIfNull(audioFormat, nameof(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 RecognizeCompleted; + + // Fired when the RecognizeAsync process completes. + public event EventHandler EmulateRecognizeCompleted; + public event EventHandler LoadGrammarCompleted; + + // The event fired when speech is detected. Used for barge-in. + public event EventHandler SpeechDetected; + + // The event fired on a recognition. + public event EventHandler SpeechRecognized; + + // The event fired on a no recognition + public event EventHandler SpeechRecognitionRejected; + public event EventHandler RecognizerUpdateReached; + + // Occurs when a spoken phrase is partially recognized. + public event EventHandler SpeechHypothesized + { + [MethodImplAttribute(MethodImplOptions.Synchronized)] + add + { + Helpers.ThrowIfNull(value, nameof(value)); + if (_speechHypothesizedDelegate == null) + { + RecoBase.SpeechHypothesized += SpeechHypothesizedProxy; + } + _speechHypothesizedDelegate += value; + } + + [MethodImplAttribute(MethodImplOptions.Synchronized)] + remove + { + Helpers.ThrowIfNull(value, nameof(value)); + _speechHypothesizedDelegate -= value; + if (_speechHypothesizedDelegate == null) + { + RecoBase.SpeechHypothesized -= SpeechHypothesizedProxy; + } + } + } + public event EventHandler AudioSignalProblemOccurred + { + [MethodImplAttribute(MethodImplOptions.Synchronized)] + add + { + Helpers.ThrowIfNull(value, nameof(value)); + if (_audioSignalProblemOccurredDelegate == null) + { + RecoBase.AudioSignalProblemOccurred += AudioSignalProblemOccurredProxy; + } + _audioSignalProblemOccurredDelegate += value; + } + + [MethodImplAttribute(MethodImplOptions.Synchronized)] + remove + { + Helpers.ThrowIfNull(value, nameof(value)); + _audioSignalProblemOccurredDelegate -= value; + if (_audioSignalProblemOccurredDelegate == null) + { + RecoBase.AudioSignalProblemOccurred -= AudioSignalProblemOccurredProxy; + } + } + } + public event EventHandler AudioLevelUpdated + { + [MethodImplAttribute(MethodImplOptions.Synchronized)] + add + { + Helpers.ThrowIfNull(value, nameof(value)); + if (_audioLevelUpdatedDelegate == null) + { + RecoBase.AudioLevelUpdated += AudioLevelUpdatedProxy; + } + _audioLevelUpdatedDelegate += value; + } + + [MethodImplAttribute(MethodImplOptions.Synchronized)] + remove + { + Helpers.ThrowIfNull(value, nameof(value)); + _audioLevelUpdatedDelegate -= value; + if (_audioLevelUpdatedDelegate == null) + { + RecoBase.AudioLevelUpdated -= AudioLevelUpdatedProxy; + } + } + } + public event EventHandler AudioStateChanged + { + [MethodImplAttribute(MethodImplOptions.Synchronized)] + add + { + Helpers.ThrowIfNull(value, nameof(value)); + if (_audioStateChangedDelegate == null) + { + RecoBase.AudioStateChanged += AudioStateChangedProxy; + } + _audioStateChangedDelegate += value; + } + + [MethodImplAttribute(MethodImplOptions.Synchronized)] + remove + { + Helpers.ThrowIfNull(value, nameof(value)); + _audioStateChangedDelegate -= value; + if (_audioStateChangedDelegate == null) + { + RecoBase.AudioStateChanged -= AudioStateChangedProxy; + } + } + } + + #endregion + + #region Private Methods + + 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) + { + EventHandler recognizeCompletedHandler = RecognizeCompleted; + if (recognizeCompletedHandler != null) + { + recognizeCompletedHandler(this, e); + } + } + + private void EmulateRecognizeCompletedProxy(object sender, EmulateRecognizeCompletedEventArgs e) + { + EventHandler emulateRecognizeCompletedHandler = EmulateRecognizeCompleted; + if (emulateRecognizeCompletedHandler != null) + { + emulateRecognizeCompletedHandler(this, e); + } + } + + private void LoadGrammarCompletedProxy(object sender, LoadGrammarCompletedEventArgs e) + { + EventHandler loadGrammarCompletedHandler = LoadGrammarCompleted; + if (loadGrammarCompletedHandler != null) + { + loadGrammarCompletedHandler(this, e); + } + } + + private void SpeechDetectedProxy(object sender, SpeechDetectedEventArgs e) + { + EventHandler speechDetectedHandler = SpeechDetected; + if (speechDetectedHandler != null) + { + speechDetectedHandler(this, e); + } + } + + private void SpeechRecognizedProxy(object sender, SpeechRecognizedEventArgs e) + { + EventHandler speechRecognizedHandler = SpeechRecognized; + if (speechRecognizedHandler != null) + { + speechRecognizedHandler(this, e); + } + } + + private void SpeechRecognitionRejectedProxy(object sender, SpeechRecognitionRejectedEventArgs e) + { + EventHandler speechRecognitionRejectedHandler = SpeechRecognitionRejected; + if (speechRecognitionRejectedHandler != null) + { + speechRecognitionRejectedHandler(this, e); + } + } + + private void RecognizerUpdateReachedProxy(object sender, RecognizerUpdateReachedEventArgs e) + { + EventHandler recognizerUpdateReachedHandler = RecognizerUpdateReached; + if (recognizerUpdateReachedHandler != null) + { + recognizerUpdateReachedHandler(this, e); + } + } + + private void SpeechHypothesizedProxy(object sender, SpeechHypothesizedEventArgs e) + { + EventHandler speechHypothesizedHandler = _speechHypothesizedDelegate; + if (speechHypothesizedHandler != null) + { + speechHypothesizedHandler(this, e); + } + } + + private void AudioSignalProblemOccurredProxy(object sender, AudioSignalProblemOccurredEventArgs e) + { + EventHandler audioSignalProblemOccurredHandler = _audioSignalProblemOccurredDelegate; + if (audioSignalProblemOccurredHandler != null) + { + audioSignalProblemOccurredHandler(this, e); + } + } + + private void AudioLevelUpdatedProxy(object sender, AudioLevelUpdatedEventArgs e) + { + EventHandler audioLevelUpdatedHandler = _audioLevelUpdatedDelegate; + if (audioLevelUpdatedHandler != null) + { + audioLevelUpdatedHandler(this, e); + } + } + + private void AudioStateChangedProxy(object sender, AudioStateChangedEventArgs e) + { + EventHandler audioStateChangedHandler = _audioStateChangedDelegate; + if (audioStateChangedHandler != null) + { + audioStateChangedHandler(this, e); + } + } + + #endregion + + #region Private Properties + private RecognizerBase RecoBase + { + get + { + if (_disposed) + { + throw new ObjectDisposedException("SpeechRecognitionEngine"); + } + if (_recognizerBase == null) + { + _recognizerBase = new RecognizerBase(); + _recognizerBase.Initialize(_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 _audioSignalProblemOccurredDelegate; + private EventHandler _audioLevelUpdatedDelegate; + private EventHandler _audioStateChangedDelegate; + + private EventHandler _speechHypothesizedDelegate; + + #endregion + } +} diff --git a/src/libraries/System.Speech/src/Recognition/SpeechRecognizer.cs b/src/libraries/System.Speech/src/Recognition/SpeechRecognizer.cs new file mode 100644 index 00000000000000..91c01424b63ec9 --- /dev/null +++ b/src/libraries/System.Speech/src/Recognition/SpeechRecognizer.cs @@ -0,0 +1,501 @@ +// 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.Runtime.CompilerServices; +using System.Runtime.InteropServices; +using System.Speech.AudioFormat; +using System.Speech.Internal; +using System.Speech.Internal.SapiInterop; + +namespace System.Speech.Recognition +{ + public class SpeechRecognizer : IDisposable + { + #region Constructors + public SpeechRecognizer() + { + _sapiRecognizer = new SapiRecognizer(SapiRecognizer.RecognizerType.Shared); + } + 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 public Properties + + // Determines whether the recognizer is listening or not. + public RecognizerState State + { + get { return RecoBase.State; } + } + + // Are the grammars attached to this SpeechRecognizer active? Default = true + public bool Enabled + { + get { return RecoBase.Enabled; } + set { RecoBase.Enabled = value; } + } + public bool PauseRecognizerOnRecognition + { + get { return RecoBase.PauseRecognizerOnRecognition; } + set { RecoBase.PauseRecognizerOnRecognition = value; } + } + + // Gives access to the collection of grammars that are currently active. Read-only. + public ReadOnlyCollection 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 AudioPosition + { + get { return RecoBase.AudioPosition; } + } + + // Data on the audio stream the recognizer is processing + public TimeSpan RecognizerAudioPosition + { + get { return RecoBase.RecognizerAudioPosition; } + } + public SpeechAudioFormatInfo AudioFormat + { + get { return RecoBase.AudioFormat; } + } + public int MaxAlternates + { + get { return RecoBase.MaxAlternates; } + set { RecoBase.MaxAlternates = value; } + } + + #endregion + + #region public Methods + 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) + { + if (Enabled) + { + return RecoBase.EmulateRecognize(inputText); + } + else + { + throw new InvalidOperationException(SR.Get(SRID.RecognizerNotEnabled)); + } + } + public RecognitionResult EmulateRecognize(string inputText, CompareOptions compareOptions) + { + if (Enabled) + { + return RecoBase.EmulateRecognize(inputText, compareOptions); + } + else + { + throw new InvalidOperationException(SR.Get(SRID.RecognizerNotEnabled)); + } + } + public RecognitionResult EmulateRecognize(RecognizedWordUnit[] wordUnits, CompareOptions compareOptions) + { + if (Enabled) + { + return RecoBase.EmulateRecognize(wordUnits, compareOptions); + } + else + { + throw new InvalidOperationException(SR.Get(SRID.RecognizerNotEnabled)); + } + } + public void EmulateRecognizeAsync(string inputText) + { + if (Enabled) + { + RecoBase.EmulateRecognizeAsync(inputText); + } + else + { + throw new InvalidOperationException(SR.Get(SRID.RecognizerNotEnabled)); + } + } + public void EmulateRecognizeAsync(string inputText, CompareOptions compareOptions) + { + if (Enabled) + { + RecoBase.EmulateRecognizeAsync(inputText, compareOptions); + } + else + { + throw new InvalidOperationException(SR.Get(SRID.RecognizerNotEnabled)); + } + } + public void EmulateRecognizeAsync(RecognizedWordUnit[] wordUnits, CompareOptions compareOptions) + { + if (Enabled) + { + RecoBase.EmulateRecognizeAsync(wordUnits, compareOptions); + } + else + { + throw new InvalidOperationException(SR.Get(SRID.RecognizerNotEnabled)); + } + } + + // 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 + public event EventHandler StateChanged; + + // Fired when the RecognizeAsync process completes. + public event EventHandler EmulateRecognizeCompleted; + public event EventHandler LoadGrammarCompleted; + + // The event fired when speech is detected. Used for barge-in. + public event EventHandler SpeechDetected; + + // The event fired on a recognition. + public event EventHandler SpeechRecognized; + + // The event fired on a no recognition + public event EventHandler SpeechRecognitionRejected; + public event EventHandler RecognizerUpdateReached; + + // Occurs when a spoken phrase is partially recognized. + public event EventHandler SpeechHypothesized + { + [MethodImplAttribute(MethodImplOptions.Synchronized)] + add + { + Helpers.ThrowIfNull(value, nameof(value)); + if (_speechHypothesizedDelegate == null) + { + RecoBase.SpeechHypothesized += SpeechHypothesizedProxy; + } + _speechHypothesizedDelegate += value; + } + + [MethodImplAttribute(MethodImplOptions.Synchronized)] + remove + { + Helpers.ThrowIfNull(value, nameof(value)); + _speechHypothesizedDelegate -= value; + if (_speechHypothesizedDelegate == null) + { + RecoBase.SpeechHypothesized -= SpeechHypothesizedProxy; + } + } + } + public event EventHandler AudioSignalProblemOccurred + { + [MethodImplAttribute(MethodImplOptions.Synchronized)] + add + { + Helpers.ThrowIfNull(value, nameof(value)); + if (_audioSignalProblemOccurredDelegate == null) + { + RecoBase.AudioSignalProblemOccurred += AudioSignalProblemOccurredProxy; + } + _audioSignalProblemOccurredDelegate += value; + } + + [MethodImplAttribute(MethodImplOptions.Synchronized)] + remove + { + Helpers.ThrowIfNull(value, nameof(value)); + _audioSignalProblemOccurredDelegate -= value; + if (_audioSignalProblemOccurredDelegate == null) + { + RecoBase.AudioSignalProblemOccurred -= AudioSignalProblemOccurredProxy; + } + } + } + public event EventHandler AudioLevelUpdated + { + [MethodImplAttribute(MethodImplOptions.Synchronized)] + add + { + Helpers.ThrowIfNull(value, nameof(value)); + if (_audioLevelUpdatedDelegate == null) + { + RecoBase.AudioLevelUpdated += AudioLevelUpdatedProxy; + } + _audioLevelUpdatedDelegate += value; + } + + [MethodImplAttribute(MethodImplOptions.Synchronized)] + remove + { + Helpers.ThrowIfNull(value, nameof(value)); + _audioLevelUpdatedDelegate -= value; + if (_audioLevelUpdatedDelegate == null) + { + RecoBase.AudioLevelUpdated -= AudioLevelUpdatedProxy; + } + } + } + public event EventHandler AudioStateChanged + { + [MethodImplAttribute(MethodImplOptions.Synchronized)] + add + { + Helpers.ThrowIfNull(value, nameof(value)); + if (_audioStateChangedDelegate == null) + { + RecoBase.AudioStateChanged += AudioStateChangedProxy; + } + _audioStateChangedDelegate += value; + } + + [MethodImplAttribute(MethodImplOptions.Synchronized)] + remove + { + Helpers.ThrowIfNull(value, nameof(value)); + _audioStateChangedDelegate -= value; + if (_audioStateChangedDelegate == null) + { + RecoBase.AudioStateChanged -= AudioStateChangedProxy; + } + } + } + + #endregion + + #region Private Methods + + // Proxy event handlers used to translate the sender from the RecognizerBase to this class: + + private void StateChangedProxy(object sender, StateChangedEventArgs e) + { + EventHandler stateChangedHandler = StateChanged; + if (stateChangedHandler != null) + { + stateChangedHandler(this, e); + } + } + + private void EmulateRecognizeCompletedProxy(object sender, EmulateRecognizeCompletedEventArgs e) + { + EventHandler emulateRecognizeCompletedHandler = EmulateRecognizeCompleted; + if (emulateRecognizeCompletedHandler != null) + { + emulateRecognizeCompletedHandler(this, e); + } + } + + private void LoadGrammarCompletedProxy(object sender, LoadGrammarCompletedEventArgs e) + { + EventHandler loadGrammarCompletedHandler = LoadGrammarCompleted; + if (loadGrammarCompletedHandler != null) + { + loadGrammarCompletedHandler(this, e); + } + } + + private void SpeechDetectedProxy(object sender, SpeechDetectedEventArgs e) + { + EventHandler speechDetectedHandler = SpeechDetected; + if (speechDetectedHandler != null) + { + speechDetectedHandler(this, e); + } + } + + private void SpeechRecognizedProxy(object sender, SpeechRecognizedEventArgs e) + { + EventHandler speechRecognizedHandler = SpeechRecognized; + if (speechRecognizedHandler != null) + { + speechRecognizedHandler(this, e); + } + } + + private void SpeechRecognitionRejectedProxy(object sender, SpeechRecognitionRejectedEventArgs e) + { + EventHandler speechRecognitionRejectedHandler = SpeechRecognitionRejected; + if (speechRecognitionRejectedHandler != null) + { + speechRecognitionRejectedHandler(this, e); + } + } + + private void RecognizerUpdateReachedProxy(object sender, RecognizerUpdateReachedEventArgs e) + { + EventHandler recognizerUpdateReachedHandler = RecognizerUpdateReached; + if (recognizerUpdateReachedHandler != null) + { + recognizerUpdateReachedHandler(this, e); + } + } + + private void SpeechHypothesizedProxy(object sender, SpeechHypothesizedEventArgs e) + { + EventHandler speechHypothesizedHandler = _speechHypothesizedDelegate; + if (speechHypothesizedHandler != null) + { + speechHypothesizedHandler(this, e); + } + } + + private void AudioSignalProblemOccurredProxy(object sender, AudioSignalProblemOccurredEventArgs e) + { + EventHandler audioSignalProblemOccurredHandler = _audioSignalProblemOccurredDelegate; + if (audioSignalProblemOccurredHandler != null) + { + audioSignalProblemOccurredHandler(this, e); + } + } + + private void AudioLevelUpdatedProxy(object sender, AudioLevelUpdatedEventArgs e) + { + EventHandler audioLevelUpdatedHandler = _audioLevelUpdatedDelegate; + if (audioLevelUpdatedHandler != null) + { + audioLevelUpdatedHandler(this, e); + } + } + + private void AudioStateChangedProxy(object sender, AudioStateChangedEventArgs e) + { + EventHandler audioStateChangedHandler = _audioStateChangedDelegate; + if (audioStateChangedHandler != null) + { + audioStateChangedHandler(this, e); + } + } + + #endregion + + #region Private Properties + private RecognizerBase RecoBase + { + get + { + if (_disposed) + { + throw new ObjectDisposedException("SpeechRecognitionEngine"); + } + + if (_recognizerBase == null) + { + _recognizerBase = new RecognizerBase(); + + try + { + _recognizerBase.Initialize(_sapiRecognizer, false); + } + catch (COMException e) + { + throw RecognizerBase.ExceptionFromSapiCreateRecognizerError(e); + } + + // This means the SpeechRecognizer will, by default, not pause after every recognition to allow updates. + PauseRecognizerOnRecognition = false; + + // We always have an input on the SpeechRecognizer. + _recognizerBase._haveInputSource = true; + + // If audio is already being processed then update AudioState. + if (AudioPosition != TimeSpan.Zero) + { + _recognizerBase.AudioState = AudioState.Silence; // Technically it might be Speech but that's okay. + } + + // For the SpeechRecognizer the RecoState is never altered: + // - By default that will mean recognition will progress as long as one grammar is loaded and enabled. + + // Add event handlers for low-overhead events: + _recognizerBase.StateChanged += StateChangedProxy; + _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 _audioSignalProblemOccurredDelegate; + private EventHandler _audioLevelUpdatedDelegate; + private EventHandler _audioStateChangedDelegate; + private EventHandler _speechHypothesizedDelegate; + + #endregion + } +} diff --git a/src/libraries/System.Speech/src/Recognition/SpeechUI.cs b/src/libraries/System.Speech/src/Recognition/SpeechUI.cs new file mode 100644 index 00000000000000..68acc806d06a04 --- /dev/null +++ b/src/libraries/System.Speech/src/Recognition/SpeechUI.cs @@ -0,0 +1,21 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Speech.Internal; + +namespace System.Speech.Recognition +{ + public class SpeechUI + { + internal SpeechUI() + { + } + public static bool SendTextFeedback(RecognitionResult result, string feedback, bool isSuccessfulAction) + { + Helpers.ThrowIfNull(result, nameof(result)); + Helpers.ThrowIfEmptyOrNull(feedback, nameof(feedback)); + + return result.SetTextFeedback(feedback, isSuccessfulAction); + } + } +} diff --git a/src/libraries/System.Speech/src/Recognition/SrgsGrammar/SrgsDocument.cs b/src/libraries/System.Speech/src/Recognition/SrgsGrammar/SrgsDocument.cs new file mode 100644 index 00000000000000..14020f6bc480df --- /dev/null +++ b/src/libraries/System.Speech/src/Recognition/SrgsGrammar/SrgsDocument.cs @@ -0,0 +1,425 @@ +// 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.Speech.Internal; +using System.Speech.Internal.SrgsCompiler; +using System.Speech.Internal.SrgsParser; +using System.Xml; + +namespace System.Speech.Recognition.SrgsGrammar +{ + /// + /// This class allows a _grammar to be specified in SRGS form. + /// APITODO: needs programmatic access to SRGS DOM; PACOG + /// APITODO: needs rule activation/deactivation methods + /// + [Serializable] + public class SrgsDocument + { + #region Constructors / Destructors + + /// + /// The default constructor - creates an empty SrgsGrammar object + /// + public SrgsDocument() + { + _grammar = new SrgsGrammar(); + } + + public SrgsDocument(string path) + { + Helpers.ThrowIfEmptyOrNull(path, nameof(path)); + + using (XmlTextReader reader = new(path)) + { + Load(reader); + } + } + public SrgsDocument(XmlReader srgsGrammar) + { + Helpers.ThrowIfNull(srgsGrammar, nameof(srgsGrammar)); + + Load(srgsGrammar); + } + public SrgsDocument(GrammarBuilder builder) + { + Helpers.ThrowIfNull(builder, nameof(builder)); + + // New grammar + _grammar = new SrgsGrammar + { +#pragma warning disable 56504 // The Culture property is the Grammar builder is already checked. + Culture = builder.Culture + }; +#pragma warning restore 56504 + + // Creates SrgsDocument elements + IElementFactory elementFactory = new SrgsElementFactory(_grammar); + + // Do it + builder.CreateGrammar(elementFactory); + } + + public SrgsDocument(SrgsRule grammarRootRule) : this() + { + Helpers.ThrowIfNull(grammarRootRule, nameof(grammarRootRule)); + + Root = grammarRootRule; + Rules.Add(grammarRootRule); + } + + #endregion + + #region public methods + public void WriteSrgs(XmlWriter srgsGrammar) + { + Helpers.ThrowIfNull(srgsGrammar, nameof(srgsGrammar)); + + // Make sure the grammar is ok + _grammar.Validate(); + + // Write the data. + _grammar.WriteSrgs(srgsGrammar); + } + + #endregion + + #region Public Properties + + /// + /// Base URI of _grammar (xml:base). + /// + public Uri XmlBase + { + get + { + return _grammar.XmlBase; + } + set + { + // base value can be null +#pragma warning disable 56526 + _grammar.XmlBase = value; +#pragma warning restore 56526 + } + } + + /// + /// Grammar language (xml:lang) + /// + public CultureInfo Culture + { + get + { + return _grammar.Culture; + } + set + { + Helpers.ThrowIfNull(value, nameof(value)); + if (value.Equals(CultureInfo.InvariantCulture)) + { + throw new ArgumentException(SR.Get(SRID.InvariantCultureInfo), nameof(value)); + } + _grammar.Culture = value; + } + } + + /// + /// Root rule (srgs:root) + /// + public SrgsRule Root + { + get + { + return _grammar.Root; + } + set + { + // base value can be null +#pragma warning disable 56526 + _grammar.Root = value; +#pragma warning restore 56526 + } + } + + /// + /// Grammar mode (srgs:mode) - voice, dtmf + /// + public SrgsGrammarMode Mode + { + get + { + return _grammar.Mode == GrammarType.VoiceGrammar ? SrgsGrammarMode.Voice : SrgsGrammarMode.Dtmf; + } + set + { + _grammar.Mode = value == SrgsGrammarMode.Voice ? GrammarType.VoiceGrammar : GrammarType.DtmfGrammar; + } + } + + /// + /// Grammar mode (srgs:mode) - voice, dtmf + /// + public SrgsPhoneticAlphabet PhoneticAlphabet + { + get + { + return (SrgsPhoneticAlphabet)_grammar.PhoneticAlphabet; + } + set + { + _grammar.PhoneticAlphabet = (AlphabetType)value; + _grammar.HasPhoneticAlphabetBeenSet = true; + } + } + + /// + /// A collection of rules that this _grammar houses. + /// + // APITODO: Implementations of Rules and all other SRGS objects not here for now + public SrgsRulesCollection Rules + { + get + { + return _grammar.Rules; + } + } + + /// + /// Programming Language used for the inline code; C#, VB or JScript + /// + public string Language + { + get + { + return _grammar.Language; + } + set + { + // Language can be set to null +#pragma warning disable 56526 + _grammar.Language = value; +#pragma warning restore 56526 + } + } + + /// + /// namespace + /// + public string Namespace + { + get + { + return _grammar.Namespace; + } + set + { + // namespace can be set to null +#pragma warning disable 56526 + _grammar.Namespace = value; +#pragma warning restore 56526 + } + } + + /// + /// CodeBehind + /// + public Collection CodeBehind + { + get + { + return _grammar.CodeBehind; + } + } + + /// + /// Add #line statements to the inline scripts if set + /// + public bool Debug + { + get + { + return _grammar.Debug; + } + set + { + _grammar.Debug = value; + } + } + + /// + /// language + /// + public string Script + { + get + { + return _grammar.Script; + } + set + { + Helpers.ThrowIfEmptyOrNull(value, nameof(value)); + _grammar.Script = value; + } + } + + /// + /// ImportNameSpaces + /// + public Collection ImportNamespaces + { + get + { + return _grammar.ImportNamespaces; + } + } + + /// + /// ImportNameSpaces + /// + public Collection AssemblyReferences + { + get + { + return _grammar.AssemblyReferences; + } + } + + #endregion + + #region Internal methods + + // Initialize an SrgsDocument from an Srgs text source. + internal void Load(XmlReader srgsGrammar) + { + // New grammar + _grammar = new SrgsGrammar + { + // For SrgsGrammar, the default is IPA, for xml grammars, it is sapi. + PhoneticAlphabet = AlphabetType.Sapi + }; + + // create an XMl Parser + XmlParser srgsParser = new(srgsGrammar, null); + + // Creates SrgsDocument elements + srgsParser.ElementFactory = new SrgsElementFactory(_grammar); + + // Do it + srgsParser.Parse(); + + // This provides the path the XML was loaded from. + // {Note potentially this may also be overridden by an xml:base attribute in the XML itself. + // But for this scenario that doesn't matter since this is used to calculate the correct base path.} + if (!string.IsNullOrEmpty(srgsGrammar.BaseURI)) + { + _baseUri = new Uri(srgsGrammar.BaseURI); + } + } + + internal static GrammarOptions TagFormat2GrammarOptions(SrgsTagFormat value) + { + GrammarOptions newValue = 0; + + switch (value) + { + case SrgsTagFormat.KeyValuePairs: + newValue = GrammarOptions.KeyValuePairSrgs; + break; + + case SrgsTagFormat.MssV1: + newValue = GrammarOptions.MssV1; + break; + + case SrgsTagFormat.W3cV1: + newValue = GrammarOptions.W3cV1; + break; + } + return newValue; + } + + internal static SrgsTagFormat GrammarOptions2TagFormat(GrammarOptions value) + { + SrgsTagFormat tagFormat = SrgsTagFormat.Default; + + switch (value & GrammarOptions.TagFormat) + { + case GrammarOptions.MssV1: + tagFormat = SrgsTagFormat.MssV1; + break; + + case GrammarOptions.W3cV1: + tagFormat = SrgsTagFormat.W3cV1; + break; + + case GrammarOptions.KeyValuePairSrgs: + case GrammarOptions.KeyValuePairs: + tagFormat = SrgsTagFormat.KeyValuePairs; + break; + } + return tagFormat; + } + + #endregion + + #region Internal Properties + + /// + /// Tag format (srgs:tag-format) + /// summary> + internal SrgsTagFormat TagFormat + { + set + { + _grammar.TagFormat = value; + } + } + + internal Uri BaseUri + { + get + { + return _baseUri; + } + } + + internal SrgsGrammar Grammar + { + get + { + return _grammar; + } + } + + #endregion + + #region Private Fields + + private SrgsGrammar _grammar; + + // Path the grammar was actually loaded from, if this exists. + // Note this is different to SrgsGrammar.XmlBase which is the value of the xml:base attribute in the document itself. + private Uri _baseUri; + + #endregion Fields + } + + #region Enumerations + // Grammar mode. Voice, Dtmf + public enum SrgsGrammarMode + { + Voice, + Dtmf + } + // Grammar mode. Voice, Dtmf + public enum SrgsPhoneticAlphabet + { + Sapi, + Ipa, + Ups + } + + #endregion Enumerations +} diff --git a/src/libraries/System.Speech/src/Recognition/SrgsGrammar/SrgsElement.cs b/src/libraries/System.Speech/src/Recognition/SrgsGrammar/SrgsElement.cs new file mode 100644 index 00000000000000..f371e01a7464b2 --- /dev/null +++ b/src/libraries/System.Speech/src/Recognition/SrgsGrammar/SrgsElement.cs @@ -0,0 +1,87 @@ +// 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; +using System.Speech.Internal.SrgsParser; +using System.Xml; + +namespace System.Speech.Recognition.SrgsGrammar +{ + /// + /// Base class for all SRGS object to build XML fragment corresponding to the object. + /// + [Serializable] + [DebuggerDisplay("SrgsElement Children:[{_items.Count}]")] + [DebuggerTypeProxy(typeof(SrgsElementDebugDisplay))] + public abstract class SrgsElement : MarshalByRefObject, IElement + { + protected SrgsElement() + { + } + + #region Internal methods + + // Write the XML fragment describing the object. + internal abstract void WriteSrgs(XmlWriter writer); + + // Debugger display string. + internal abstract string DebuggerDisplayString(); + + // Validate the SRGS element. + /// + /// Validate each element and recurse through all the children srgs + /// elements if any. + /// Any derived class implementing this method must call the base class + /// in order for the children to be processed. + /// + internal virtual void Validate(SrgsGrammar grammar) + { + foreach (SrgsElement element in Children) + { + // Child validation + element.Validate(grammar); + } + } + + void IElement.PostParse(IElement parent) + { + } + + #endregion + + #region Protected Properties + + internal virtual SrgsElement[] Children + { + get + { + return Array.Empty(); + } + } + + #endregion + + #region Private Types + + // Used by the debugger display attribute + internal class SrgsElementDebugDisplay + { + public SrgsElementDebugDisplay(SrgsElement element) + { + _elements = element.Children; + } + [DebuggerBrowsable(DebuggerBrowsableState.RootHidden)] + public SrgsElement[] AKeys + { + get + { + return _elements; + } + } + + private SrgsElement[] _elements; + } + + #endregion + } +} diff --git a/src/libraries/System.Speech/src/Recognition/SrgsGrammar/SrgsElementFactory.cs b/src/libraries/System.Speech/src/Recognition/SrgsGrammar/SrgsElementFactory.cs new file mode 100644 index 00000000000000..06af30b0818674 --- /dev/null +++ b/src/libraries/System.Speech/src/Recognition/SrgsGrammar/SrgsElementFactory.cs @@ -0,0 +1,221 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +#region Using directives + +using System.Speech.Internal; +using System.Speech.Internal.SrgsParser; + +#endregion + +namespace System.Speech.Recognition.SrgsGrammar +{ + internal class SrgsElementFactory : IElementFactory + { + internal SrgsElementFactory(SrgsGrammar grammar) + { + _grammar = grammar; + } + + /// + /// Clear all the rules + /// + void IElementFactory.RemoveAllRules() + { + } + + IPropertyTag IElementFactory.CreatePropertyTag(IElement parent) + { + return new SrgsNameValueTag(); + } + + ISemanticTag IElementFactory.CreateSemanticTag(IElement parent) + { + return new SrgsSemanticInterpretationTag(); + } + + IElementText IElementFactory.CreateText(IElement parent, string value) + { + return new SrgsText(value); + } + + IToken IElementFactory.CreateToken(IElement parent, string content, string pronunciation, string display, float reqConfidence) + { + SrgsToken token = new(content); + if (!string.IsNullOrEmpty(pronunciation)) + { + // Check if the pronunciations are ok + string sPron = pronunciation; + for (int iCurPron = 0, iDeliminator = 0; iCurPron < sPron.Length; iCurPron = iDeliminator + 1) + { + // Find semi-colon delimiter and replace with null + iDeliminator = pronunciation.IndexOfAny(s_pronSeparator, iCurPron); + if (iDeliminator == -1) + { + iDeliminator = sPron.Length; + } + + string sSubPron = sPron.Substring(iCurPron, iDeliminator - iCurPron); + + // make sure this goes through + switch (_grammar.PhoneticAlphabet) + { + case AlphabetType.Sapi: + sSubPron = PhonemeConverter.ConvertPronToId(sSubPron, _grammar.Culture.LCID); + break; + + case AlphabetType.Ipa: + PhonemeConverter.ValidateUpsIds(sSubPron); + break; + + case AlphabetType.Ups: + sSubPron = PhonemeConverter.UpsConverter.ConvertPronToId(sSubPron); + break; + } + } + + token.Pronunciation = pronunciation; + } + + if (!string.IsNullOrEmpty(display)) + { + token.Display = display; + } + + if (reqConfidence >= 0) + { + throw new NotSupportedException(SR.Get(SRID.ReqConfidenceNotSupported)); + } + return token; + } + + IItem IElementFactory.CreateItem(IElement parent, IRule rule, int minRepeat, int maxRepeat, float repeatProbability, float weight) + { + SrgsItem item = new(); + if (minRepeat != 1 || maxRepeat != 1) + { + item.SetRepeat(minRepeat, maxRepeat); + } + item.RepeatProbability = repeatProbability; + item.Weight = weight; + return item; + } + + IRuleRef IElementFactory.CreateRuleRef(IElement parent, Uri srgsUri) + { + return new SrgsRuleRef(srgsUri); + } + + IRuleRef IElementFactory.CreateRuleRef(IElement parent, Uri srgsUri, string semanticKey, string parameters) + { + return new SrgsRuleRef(semanticKey, parameters, srgsUri); + } + + IOneOf IElementFactory.CreateOneOf(IElement parent, IRule rule) + { + return new SrgsOneOf(); + } + + ISubset IElementFactory.CreateSubset(IElement parent, string text, MatchMode matchMode) + { + SubsetMatchingMode matchingMode = SubsetMatchingMode.Subsequence; + + switch (matchMode) + { + case MatchMode.OrderedSubset: + matchingMode = SubsetMatchingMode.OrderedSubset; + break; + + case MatchMode.OrderedSubsetContentRequired: + matchingMode = SubsetMatchingMode.OrderedSubsetContentRequired; + break; + + case MatchMode.Subsequence: + matchingMode = SubsetMatchingMode.Subsequence; + break; + + case MatchMode.SubsequenceContentRequired: + matchingMode = SubsetMatchingMode.SubsequenceContentRequired; + break; + } + return new SrgsSubset(text, matchingMode); + } + + void IElementFactory.InitSpecialRuleRef(IElement parent, IRuleRef special) + { + } + + void IElementFactory.AddScript(IGrammar grammar, string sRule, string code) + { + SrgsGrammar srgsGrammar = (SrgsGrammar)grammar; + SrgsRule rule = srgsGrammar.Rules[sRule]; + if (rule != null) + { + rule.Script = rule.Script + code; + } + else + { + srgsGrammar.AddScript(sRule, code); + } + } + + string IElementFactory.AddScript(IGrammar grammar, string sRule, string code, string filename, int line) + { + return code; + } + + void IElementFactory.AddScript(IGrammar grammar, string script, string filename, int line) + { + SrgsGrammar srgsGrammar = (SrgsGrammar)grammar; + srgsGrammar.AddScript(null, script); + } + + void IElementFactory.AddItem(IOneOf oneOf, IItem value) + { + ((SrgsOneOf)oneOf).Add((SrgsItem)value); + } + + void IElementFactory.AddElement(IRule rule, IElement value) + { + ((SrgsRule)rule).Elements.Add((SrgsElement)value); + } + + void IElementFactory.AddElement(IItem item, IElement value) + { + ((SrgsItem)item).Elements.Add((SrgsElement)value); + } + + IGrammar IElementFactory.Grammar + { + get + { + return _grammar; + } + } + + IRuleRef IElementFactory.Null + { + get + { + return SrgsRuleRef.Null; + } + } + IRuleRef IElementFactory.Void + { + get + { + return SrgsRuleRef.Void; + } + } + IRuleRef IElementFactory.Garbage + { + get + { + return SrgsRuleRef.Garbage; + } + } + private SrgsGrammar _grammar; + + private static readonly char[] s_pronSeparator = new char[] { ' ', '\t', '\n', '\r', ';' }; + } +} diff --git a/src/libraries/System.Speech/src/Recognition/SrgsGrammar/SrgsElementList.cs b/src/libraries/System.Speech/src/Recognition/SrgsGrammar/SrgsElementList.cs new file mode 100644 index 00000000000000..2bfaeaf1dc08a7 --- /dev/null +++ b/src/libraries/System.Speech/src/Recognition/SrgsGrammar/SrgsElementList.cs @@ -0,0 +1,23 @@ +// 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.Speech.Internal; + +namespace System.Speech.Recognition.SrgsGrammar +{ + [Serializable] + internal class SrgsElementList : Collection + { + #region Interfaces Implementations + + protected override void InsertItem(int index, SrgsElement element) + { + Helpers.ThrowIfNull(element, nameof(element)); + + base.InsertItem(index, element); + } + + #endregion + } +} diff --git a/src/libraries/System.Speech/src/Recognition/SrgsGrammar/SrgsGrammar.cs b/src/libraries/System.Speech/src/Recognition/SrgsGrammar/SrgsGrammar.cs new file mode 100644 index 00000000000000..7837767ebc84f9 --- /dev/null +++ b/src/libraries/System.Speech/src/Recognition/SrgsGrammar/SrgsGrammar.cs @@ -0,0 +1,698 @@ +// 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.Globalization; +using System.Speech.Internal; +using System.Speech.Internal.SrgsParser; +using System.Xml; + +#pragma warning disable 56500 // Remove all the catch all statements warnings used by the interop layer + +namespace System.Speech.Recognition.SrgsGrammar +{ + [Serializable] + internal sealed class SrgsGrammar : IGrammar + { + #region Constructors + + /// + /// Initializes a new instance of the Grammar class. + /// + internal SrgsGrammar() + { + _rules = new SrgsRulesCollection(); + } + + #endregion + + #region Internal Methods + + /// + /// Write the XML fragment describing the object. + /// + /// XmlWriter to which to write the XML fragment. + internal void WriteSrgs(XmlWriter writer) + { + // Write + writer.WriteStartElement("grammar", XmlParser.srgsNamespace); + writer.WriteAttributeString("xml", "lang", null, _culture.ToString()); + + if (_root != null) + { + writer.WriteAttributeString("root", _root.Id); + } + + // Write the attributes for strongly typed grammars + WriteSTGAttributes(writer); + if (_isModeSet) + { + switch (_mode) + { + case SrgsGrammarMode.Voice: + writer.WriteAttributeString("mode", "voice"); + break; + + case SrgsGrammarMode.Dtmf: + writer.WriteAttributeString("mode", "dtmf"); + break; + } + } + + // Write the tag format if any + string tagFormat = null; + switch (_tagFormat) + { + case SrgsTagFormat.Default: + // Nothing to do + break; + + case SrgsTagFormat.MssV1: + tagFormat = "semantics-ms/1.0"; + break; + + case SrgsTagFormat.W3cV1: + tagFormat = "semantics/1.0"; + break; + + case SrgsTagFormat.KeyValuePairs: + tagFormat = "properties-ms/1.0"; + break; + + default: + System.Diagnostics.Debug.Assert(false, "Unknown Tag Format!!!"); + break; + } + + if (tagFormat != null) + { + writer.WriteAttributeString("tag-format", tagFormat); + } + + // Write the Alphabet type if not SAPI + if (_hasPhoneticAlphabetBeenSet || (_phoneticAlphabet != SrgsPhoneticAlphabet.Sapi && HasPronunciation)) + { + string alphabet = _phoneticAlphabet == SrgsPhoneticAlphabet.Ipa ? "ipa" : _phoneticAlphabet == SrgsPhoneticAlphabet.Ups ? "x-microsoft-ups" : "x-microsoft-sapi"; + + writer.WriteAttributeString("sapi", "alphabet", XmlParser.sapiNamespace, alphabet); + } + + if (_xmlBase != null) + { + writer.WriteAttributeString("xml:base", _xmlBase.ToString()); + } + + writer.WriteAttributeString("version", "1.0"); + + writer.WriteAttributeString("xmlns", XmlParser.srgsNamespace); + + if (_isSapiExtensionUsed) + { + writer.WriteAttributeString("xmlns", "sapi", null, XmlParser.sapiNamespace); + } + + foreach (SrgsRule rule in _rules) + { + // Validate child _rules + rule.Validate(this); + } + + // Write the tag elements if any + foreach (string tag in _globalTags) + { + writer.WriteElementString("tag", tag); + } + + //Write the references to the referenced assemblies and the various scripts + WriteGrammarElements(writer); + + writer.WriteEndElement(); + } + + /// + /// Validate the SRGS element. + /// + internal void Validate() + { + // Validation set the pronunciation so reset it to zero + HasPronunciation = HasSapiExtension = false; + + // validate all the rules + foreach (SrgsRule rule in _rules) + { + // Validate child _rules + rule.Validate(this); + } + + // Initial values for ContainsCOde and SapiExtensionUsed. + _isSapiExtensionUsed |= HasPronunciation; + _fContainsCode |= _language != null || _script.Length > 0 || _usings.Count > 0 || _assemblyReferences.Count > 0 || _codebehind.Count > 0 || _namespace != null || _fDebug; + _isSapiExtensionUsed |= _fContainsCode; + // If the grammar contains no pronunciations, set the phonetic alphabet to SAPI. + // This way, the CFG data can be loaded by SAPI 5.1. + if (!HasPronunciation) + { + PhoneticAlphabet = AlphabetType.Sapi; + } + + // Validate root rule reference + if (_root != null) + { + if (!_rules.Contains(_root)) + { + XmlParser.ThrowSrgsException(SRID.RootNotDefined, _root.Id); + } + } + + if (_globalTags.Count > 0) + { + _tagFormat = SrgsTagFormat.W3cV1; + } + + // Force the tag format to Sapi properties if .NET semantics are used. + if (_fContainsCode) + { + if (_tagFormat == SrgsTagFormat.Default) + { + _tagFormat = SrgsTagFormat.KeyValuePairs; + } + + // SAPI semantics only for .NET Semantics + if (_tagFormat != SrgsTagFormat.KeyValuePairs) + { + XmlParser.ThrowSrgsException(SRID.InvalidSemanticProcessingType); + } + } + } + + IRule IGrammar.CreateRule(string id, RulePublic publicRule, RuleDynamic dynamic, bool hasScript) + { + SrgsRule rule = new(id); + if (publicRule != RulePublic.NotSet) + { + rule.Scope = publicRule == RulePublic.True ? SrgsRuleScope.Public : SrgsRuleScope.Private; + } + rule.Dynamic = dynamic; + return rule; + } + + void IElement.PostParse(IElement parent) + { + // Check that the root rule is defined + if (_sRoot != null) + { + bool found = false; + foreach (SrgsRule rule in Rules) + { + if (rule.Id == _sRoot) + { + Root = rule; + found = true; + break; + } + } + if (!found) + { + // "Root rule ""%s"" is undefined." + XmlParser.ThrowSrgsException(SRID.RootNotDefined, _sRoot); + } + } + + // Resolve the references to the scripts + foreach (XmlParser.ForwardReference script in _scriptsForwardReference) + { + SrgsRule rule = Rules[script._name]; + if (rule != null) + { + rule.Script = rule.Script + script._value; + } + else + { + XmlParser.ThrowSrgsException(SRID.InvalidScriptDefinition); + } + } + // Validate the whole grammar + Validate(); + } + +#pragma warning disable 56507 // check for null or empty strings + + // Add a script to this grammar or to a rule + internal void AddScript(string rule, string code) + { + if (rule == null) + { + _script += code; + } + else + { + _scriptsForwardReference.Add(new XmlParser.ForwardReference(rule, code)); + } + } + + #endregion + + #region Internal Properties + + /// + /// Sets the Root element + /// + string IGrammar.Root + { + get + { + return _sRoot; + } + set + { + _sRoot = value; + } + } + + /// + /// Base URI of grammar (xml:base) + /// + public Uri XmlBase + { + get + { + return _xmlBase; + } + set + { + _xmlBase = value; + } + } + + /// + /// Grammar language (xml:lang) + /// + public CultureInfo Culture + { + get + { + return _culture; + } + set + { + Helpers.ThrowIfNull(value, nameof(value)); + + _culture = value; + } + } + + /// + /// Grammar mode. voice or dtmf + /// + public GrammarType Mode + { + get + { + return _mode == SrgsGrammarMode.Voice ? GrammarType.VoiceGrammar : GrammarType.DtmfGrammar; + } + set + { + _mode = value == GrammarType.VoiceGrammar ? SrgsGrammarMode.Voice : SrgsGrammarMode.Dtmf; + _isModeSet = true; + } + } + + /// + /// Pronunciation Alphabet, IPA or SAPI or UPS + /// + public AlphabetType PhoneticAlphabet + { + get + { + return (AlphabetType)_phoneticAlphabet; + } + set + { + _phoneticAlphabet = (SrgsPhoneticAlphabet)value; + } + } + + /// root + /// Root rule (srgs:root) + /// + public SrgsRule Root + { + get + { + return _root; + } + set + { + _root = value; + } + } + + /// + /// Tag format (srgs:tag-format) + /// + public SrgsTagFormat TagFormat + { + get + { + return _tagFormat; + } + set + { + _tagFormat = value; + } + } + + /// + /// Tag format (srgs:tag-format) + /// + public Collection GlobalTags + { + get + { + return _globalTags; + } + set + { + _globalTags = value; + } + } + + /// + /// language + /// + public string Language + { + get + { + return _language; + } + set + { + _language = value; + } + } + + /// + /// namespace + /// + public string Namespace + { + get + { + return _namespace; + } + set + { + _namespace = value; + } + } + + /// + /// CodeBehind + /// + public Collection CodeBehind + { + get + { + return _codebehind; + } + set + { + throw new InvalidOperationException(); + } + } + + /// + /// Add #line statements to the inline scripts if set + /// + public bool Debug + { + get + { + return _fDebug; + } + set + { + _fDebug = value; + } + } + + /// + /// Scripts + /// + public string Script + { + get + { + return _script; + } + set + { + Helpers.ThrowIfEmptyOrNull(value, nameof(value)); + _script = value; + } + } + + /// + /// ImportNameSpaces + /// + public Collection ImportNamespaces + { + get + { + return _usings; + } + set + { + throw new InvalidOperationException(); + } + } + + /// + /// ImportNameSpaces + /// + public Collection AssemblyReferences + { + get + { + return _assemblyReferences; + } + set + { + throw new InvalidOperationException(); + } + } + #endregion + + #region Internal Properties + + /// + /// A collection of _rules that this grammar houses. + /// + internal SrgsRulesCollection Rules + { + get + { + return _rules; + } + } + + /// + /// A collection of _rules that this grammar houses. + /// + internal bool HasPronunciation + { + get + { + return _hasPronunciation; + } + set + { + _hasPronunciation = value; + } + } + + /// + /// A collection of _rules that this grammar houses. + /// + internal bool HasPhoneticAlphabetBeenSet + { + set + { + _hasPhoneticAlphabetBeenSet = value; + } + } + + /// + /// A collection of _rules that this grammar houses. + /// + internal bool HasSapiExtension + { + get + { + return _isSapiExtensionUsed; + } + set + { + _isSapiExtensionUsed = value; + } + } + + #endregion + + #region Private Methods + + /// + /// Write the attributes of the grammar element for strongly typed grammars + /// + private void WriteSTGAttributes(XmlWriter writer) + { + // Write the 'language' attribute + if (_language != null) + { + writer.WriteAttributeString("sapi", "language", XmlParser.sapiNamespace, _language); + } + + // Write the 'namespace' attribute + if (_namespace != null) + { + writer.WriteAttributeString("sapi", "namespace", XmlParser.sapiNamespace, _namespace); + } + + // Write the 'codebehind' attribute + foreach (string sFile in _codebehind) + { + if (!string.IsNullOrEmpty(sFile)) + { + writer.WriteAttributeString("sapi", "codebehind", XmlParser.sapiNamespace, sFile); + } + } + + // Write the 'debug' attribute + if (_fDebug) + { + writer.WriteAttributeString("sapi", "debug", XmlParser.sapiNamespace, "True"); + } + } + + /// + /// Write the references to the referenced assemblies and the various scripts + /// + private void WriteGrammarElements(XmlWriter writer) + { + // Write all the entries + foreach (string sAssembly in _assemblyReferences) + { + writer.WriteStartElement("sapi", "assemblyReference", XmlParser.sapiNamespace); + writer.WriteAttributeString("sapi", "assembly", XmlParser.sapiNamespace, sAssembly); + writer.WriteEndElement(); + } + + // Write all the entries + foreach (string sNamespace in _usings) + { + if (!string.IsNullOrEmpty(sNamespace)) + { + writer.WriteStartElement("sapi", "importNamespace", XmlParser.sapiNamespace); + writer.WriteAttributeString("sapi", "namespace", XmlParser.sapiNamespace, sNamespace); + writer.WriteEndElement(); + } + } + // Then write the rules + WriteRules(writer); + + // At the very bottom write the scripts shared by all the rules + WriteGlobalScripts(writer); + } + + /// + /// Write all Rules. + /// + private void WriteRules(XmlWriter writer) + { + // Write body and footer. + foreach (SrgsRule rule in _rules) + { + rule.WriteSrgs(writer); + } + } + + /// + /// Write the script that are global to this grammar + /// + private void WriteGlobalScripts(XmlWriter writer) + { + if (_script.Length > 0) + { + writer.WriteStartElement("sapi", "script", XmlParser.sapiNamespace); + writer.WriteCData(_script); + writer.WriteEndElement(); + } + } + #endregion + + #region Private Fields + + private bool _isSapiExtensionUsed; // Set in *.Validate() + + private Uri _xmlBase; + + private CultureInfo _culture = CultureInfo.CurrentUICulture; + + private SrgsGrammarMode _mode = SrgsGrammarMode.Voice; + + private SrgsPhoneticAlphabet _phoneticAlphabet = SrgsPhoneticAlphabet.Ipa; + + private bool _hasPhoneticAlphabetBeenSet; + + private bool _hasPronunciation; + + private SrgsRule _root; + + private SrgsTagFormat _tagFormat = SrgsTagFormat.Default; + + private Collection _globalTags = new(); + + private bool _isModeSet; + + private SrgsRulesCollection _rules; + + private string _sRoot; + + internal bool _fContainsCode; // Set in *.Validate() + + // .NET Language for this grammar + private string _language; + + // .NET Language for this grammar + private Collection _codebehind = new(); + + // namespace for the code behind + private string _namespace; + + // Insert #line statements in the sources code if set + internal bool _fDebug; + + // .NET language script + private string _script = string.Empty; + + // .NET language script + private List _scriptsForwardReference = new(); + + // .NET Namespaces to import + private Collection _usings = new(); + + // .NET Namespaces to import + private Collection _assemblyReferences = new(); + #endregion + + } +} diff --git a/src/libraries/System.Speech/src/Recognition/SrgsGrammar/SrgsGrammarCompiler.cs b/src/libraries/System.Speech/src/Recognition/SrgsGrammar/SrgsGrammarCompiler.cs new file mode 100644 index 00000000000000..330cd054026f07 --- /dev/null +++ b/src/libraries/System.Speech/src/Recognition/SrgsGrammar/SrgsGrammarCompiler.cs @@ -0,0 +1,158 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.IO; +using System.Speech.Internal; +using System.Speech.Internal.SrgsCompiler; +using System.Xml; + +namespace System.Speech.Recognition.SrgsGrammar +{ + /// + /// Compiles Xml Srgs data into a CFG + /// + + public static class SrgsGrammarCompiler + { + #region Public Methods + + /// + /// Compiles a grammar to a file + /// + public static void Compile(string inputPath, Stream outputStream) + { + Helpers.ThrowIfEmptyOrNull(inputPath, nameof(inputPath)); + Helpers.ThrowIfNull(outputStream, nameof(outputStream)); + + using (XmlTextReader reader = new(new Uri(inputPath, UriKind.RelativeOrAbsolute).ToString())) + { + SrgsCompiler.CompileStream(new XmlReader[] { reader }, null, outputStream, true, null, null, null); + } + } + + /// + /// Compiles an Srgs document to a file + /// + public static void Compile(SrgsDocument srgsGrammar, Stream outputStream) + { + Helpers.ThrowIfNull(srgsGrammar, nameof(srgsGrammar)); + Helpers.ThrowIfNull(outputStream, nameof(outputStream)); + + SrgsCompiler.CompileStream(srgsGrammar, null, outputStream, true, null, null); + } + + /// + /// Compiles a grammar to a file + /// + public static void Compile(XmlReader reader, Stream outputStream) + { + Helpers.ThrowIfNull(reader, nameof(reader)); + Helpers.ThrowIfNull(outputStream, nameof(outputStream)); + + SrgsCompiler.CompileStream(new XmlReader[] { reader }, null, outputStream, true, null, null, null); + } + + /// + /// Compiles a grammar to a file + /// + public static void CompileClassLibrary(string[] inputPaths, string outputPath, string[] referencedAssemblies, string keyFile) + { + Helpers.ThrowIfNull(inputPaths, nameof(inputPaths)); + Helpers.ThrowIfEmptyOrNull(outputPath, nameof(outputPath)); + + XmlTextReader[] readers = new XmlTextReader[inputPaths.Length]; + try + { + for (int iFile = 0; iFile < inputPaths.Length; iFile++) + { + if (inputPaths[iFile] == null) + { + throw new ArgumentException(SR.Get(SRID.ArrayOfNullIllegal), nameof(inputPaths)); + } + readers[iFile] = new XmlTextReader(new Uri(inputPaths[iFile], UriKind.RelativeOrAbsolute).ToString()); + } + SrgsCompiler.CompileStream(readers, outputPath, null, false, null, referencedAssemblies, keyFile); + } + finally + { + for (int iReader = 0; iReader < readers.Length; iReader++) + { + XmlTextReader srgsGrammar = readers[iReader]; + if (srgsGrammar != null) + { + ((IDisposable)srgsGrammar).Dispose(); + } + } + } + } + + /// + /// Compiles an Srgs document to a file + /// + public static void CompileClassLibrary(SrgsDocument srgsGrammar, string outputPath, string[] referencedAssemblies, string keyFile) + { + Helpers.ThrowIfNull(srgsGrammar, nameof(srgsGrammar)); + Helpers.ThrowIfEmptyOrNull(outputPath, nameof(outputPath)); + + SrgsCompiler.CompileStream(srgsGrammar, outputPath, null, false, referencedAssemblies, keyFile); + } + + /// + /// Compiles a grammar to a file + /// + public static void CompileClassLibrary(XmlReader reader, string outputPath, string[] referencedAssemblies, string keyFile) + { + Helpers.ThrowIfNull(reader, nameof(reader)); + Helpers.ThrowIfEmptyOrNull(outputPath, nameof(outputPath)); + + SrgsCompiler.CompileStream(new XmlReader[] { reader }, outputPath, null, false, null, referencedAssemblies, keyFile); + } + + #endregion + + #region Internal Methods + + // Decide if the input stream is a cfg. + // If not assume it's an xml grammar. + // The stream parameter points to the start of the data on entry and is reset to that point on exit. + private static bool CheckIfCfg(Stream stream, out int cfgLength) + { + long initialPosition = stream.Position; + + bool isCfg = CfgGrammar.CfgSerializedHeader.IsCfg(stream, out cfgLength); + + // Reset stream position: + stream.Position = initialPosition; + return isCfg; + } + + internal static void CompileXmlOrCopyCfg( + Stream inputStream, + Stream outputStream, + Uri orginalUri) + { + // Wrap stream in case Seek is not supported: + SeekableReadStream seekableInputStream = new(inputStream); + + // See if CFG or XML document: + int cfgLength; + bool isCFG = CheckIfCfg(seekableInputStream, out cfgLength); + + seekableInputStream.CacheDataForSeeking = false; // Stop buffering data + + if (isCFG) + { + // Just copy the input to the output: + // {We later check the header on the output stream - we could do it on the input stream but it may not be seekable}. + Helpers.CopyStream(seekableInputStream, outputStream, cfgLength); + } + else + { + // Else compile the Xml: + SrgsCompiler.CompileStream(new XmlReader[] { new XmlTextReader(seekableInputStream) }, null, outputStream, true, orginalUri, null, null); + } + } + + #endregion + } +} diff --git a/src/libraries/System.Speech/src/Recognition/SrgsGrammar/SrgsItem.cs b/src/libraries/System.Speech/src/Recognition/SrgsGrammar/SrgsItem.cs new file mode 100644 index 00000000000000..c26fb7638ef737 --- /dev/null +++ b/src/libraries/System.Speech/src/Recognition/SrgsGrammar/SrgsItem.cs @@ -0,0 +1,396 @@ +// 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.Diagnostics; +using System.Globalization; +using System.Speech.Internal; +using System.Speech.Internal.SrgsParser; +using System.Text; +using System.Xml; + +namespace System.Speech.Recognition.SrgsGrammar +{ + [Serializable] + [DebuggerDisplay("{DebuggerDisplayString ()}")] + [DebuggerTypeProxy(typeof(SrgsItemDebugDisplay))] + public class SrgsItem : SrgsElement, IItem + { + #region Constructors + public SrgsItem() + { + _elements = new SrgsElementList(); + } + public SrgsItem(string text) + : this() + { + Helpers.ThrowIfEmptyOrNull(text, nameof(text)); + + _elements.Add(new SrgsText(text)); + } + public SrgsItem(params SrgsElement[] elements) + : this() + { + Helpers.ThrowIfNull(elements, nameof(elements)); + + for (int iElement = 0; iElement < elements.Length; iElement++) + { + if (elements[iElement] == null) + { + throw new ArgumentNullException(nameof(elements), SR.Get(SRID.ParamsEntryNullIllegal)); + } + _elements.Add(elements[iElement]); + } + } + public SrgsItem(int repeatCount) + : this() + { + SetRepeat(repeatCount); + } + public SrgsItem(int min, int max) + : this() + { + SetRepeat(min, max); + } + + //overloads with setting the repeat. + public SrgsItem(int min, int max, string text) + : this(text) + { + SetRepeat(min, max); + } + public SrgsItem(int min, int max, params SrgsElement[] elements) + : this(elements) + { + SetRepeat(min, max); + } + + #endregion + + #region Public Method + public void SetRepeat(int count) + { + // Negative values are not allowed + if (count < 0 || count > 255) + { + throw new ArgumentOutOfRangeException(nameof(count)); + } + _minRepeat = _maxRepeat = count; + } + public void SetRepeat(int minRepeat, int maxRepeat) + { + // Negative values are not allowed + if (minRepeat < 0 || minRepeat > 255) + { + throw new ArgumentOutOfRangeException(nameof(minRepeat), SR.Get(SRID.InvalidMinRepeat, minRepeat)); + } + if (maxRepeat != int.MaxValue && (maxRepeat < 0 || maxRepeat > 255)) + { + throw new ArgumentOutOfRangeException(nameof(maxRepeat), SR.Get(SRID.InvalidMinRepeat, maxRepeat)); + } + + // Max be greater or equal to min + if (minRepeat > maxRepeat) + { + throw new ArgumentException(SR.Get(SRID.MinGreaterThanMax)); + } + _minRepeat = minRepeat; + _maxRepeat = maxRepeat; + } + public void Add(SrgsElement element) + { + Helpers.ThrowIfNull(element, nameof(element)); + + Elements.Add(element); + } + + #endregion + + #region Public Properties + public Collection Elements + { + get + { + return _elements; + } + } + // The probability that this item will be repeated. + public float RepeatProbability + { + get + { + return _repeatProbability; + } + set + { + if (value < 0.0f || value > 1.0f) + { + throw new ArgumentOutOfRangeException(nameof(value), SR.Get(SRID.InvalidRepeatProbability, value)); + } + + _repeatProbability = value; + } + } + // The minimum number of occurrences this item can/must be repeated. + public int MinRepeat + { + get + { + return _minRepeat == NotSet ? 1 : _minRepeat; + } + } + // The maximum number of occurrences this item can/must be repeated. + public int MaxRepeat + { + get + { + return _maxRepeat == NotSet ? 1 : _maxRepeat; + } + } + public float Weight + { + get + { + return _weight; + } + set + { + if (value <= 0.0f) + { + throw new ArgumentOutOfRangeException(nameof(value), SR.Get(SRID.InvalidWeight, value)); + } + + _weight = value; + } + } + + #endregion + + #region Internal Methods + + /// + /// Write the XML fragment describing the object. + /// + internal override void WriteSrgs(XmlWriter writer) + { + // Write + writer.WriteStartElement("item"); + if (!_weight.Equals(1.0f)) + { + writer.WriteAttributeString("weight", _weight.ToString("0.########", CultureInfo.InvariantCulture)); + } + + if (!_repeatProbability.Equals(0.5f)) + { + writer.WriteAttributeString("repeat-prob", _repeatProbability.ToString("0.########", CultureInfo.InvariantCulture)); + } + + if (_minRepeat == _maxRepeat) + { + // could be because both value are NotSet of equal + if (_minRepeat != NotSet) + { + writer.WriteAttributeString("repeat", string.Format(CultureInfo.InvariantCulture, "{0}", _minRepeat)); + } + } + else if (_maxRepeat == int.MaxValue || _maxRepeat == NotSet) + { + // MinValue Set but not Max Value + writer.WriteAttributeString("repeat", string.Format(CultureInfo.InvariantCulture, "{0}-", _minRepeat)); + } + else + { + // Max Value Set and maybe MinValue + int minRepeat = _minRepeat == NotSet ? 1 : _minRepeat; + writer.WriteAttributeString("repeat", string.Format(CultureInfo.InvariantCulture, "{0}-{1}", minRepeat, _maxRepeat)); + } + + // Write body and footer. + Type previousElementType = null; + + foreach (SrgsElement element in _elements) + { + // Insert space between consecutive SrgsText _elements. + Type elementType = element.GetType(); + + if ((elementType == typeof(SrgsText)) && (elementType == previousElementType)) + { + writer.WriteString(" "); + } + + previousElementType = elementType; + element.WriteSrgs(writer); + } + + writer.WriteEndElement(); + } + + internal override string DebuggerDisplayString() + { + StringBuilder sb = new(); + + if (_elements.Count > 7) + { + sb.Append("SrgsItem Count = "); + sb.Append(_elements.Count.ToString(CultureInfo.InvariantCulture)); + } + else + { + if (_minRepeat != _maxRepeat || _maxRepeat != NotSet) + { + sb.Append('['); + if (_minRepeat == _maxRepeat) + { + sb.Append(_minRepeat.ToString(CultureInfo.InvariantCulture)); + } + else if (_maxRepeat == int.MaxValue || _maxRepeat == NotSet) + { + // MinValue Set but not Max Value + sb.Append(string.Format(CultureInfo.InvariantCulture, "{0},-", _minRepeat)); + } + else + { + // Max Value Set and maybe MinValue + int minRepeat = _minRepeat == NotSet ? 1 : _minRepeat; + sb.Append(string.Format(CultureInfo.InvariantCulture, "{0},{1}", minRepeat, _maxRepeat)); + } + sb.Append("] "); + } + + bool first = true; + foreach (SrgsElement element in _elements) + { + if (!first) + { + sb.Append(' '); + } + sb.Append('{'); + sb.Append(element.DebuggerDisplayString()); + sb.Append('}'); + first = false; + } + } + return sb.ToString(); + } + + #endregion + + #region Protected Properties + + /// + /// Allows the Srgs Element base class to implement + /// features requiring recursion in the elements tree. + /// + internal override SrgsElement[] Children + { + get + { + SrgsElement[] elements = new SrgsElement[_elements.Count]; + int i = 0; + foreach (SrgsElement element in _elements) + { + elements[i++] = element; + } + return elements; + } + } + + #endregion + + #region Private Methods + + #endregion + + #region Private Fields + + private float _weight = 1.0f; + + private float _repeatProbability = 0.5f; + + private int _minRepeat = NotSet; + + private int _maxRepeat = NotSet; + + private SrgsElementList _elements; + + private const int NotSet = -1; + + #endregion + + #region Private Types + + // Used by the debugger display attribute + internal class SrgsItemDebugDisplay + { + public SrgsItemDebugDisplay(SrgsItem item) + { + _weight = item._weight; + _repeatProbability = item._repeatProbability; + _minRepeat = item._minRepeat; + _maxRepeat = item._maxRepeat; + _elements = item._elements; + } + + public object Weight + { + get + { + return _weight; + } + } + + public object MinRepeat + { + get + { + return _minRepeat; + } + } + + public object MaxRepeat + { + get + { + return _maxRepeat; + } + } + + public object RepeatProbability + { + get + { + return _repeatProbability; + } + } + + public object Count + { + get + { + return _elements.Count; + } + } + [DebuggerBrowsable(DebuggerBrowsableState.RootHidden)] + public SrgsElement[] AKeys + { + get + { + SrgsElement[] elements = new SrgsElement[_elements.Count]; + for (int i = 0; i < _elements.Count; i++) + { + elements[i] = _elements[i]; + } + return elements; + } + } + + private float _weight = 1.0f; + private float _repeatProbability = 0.5f; + private int _minRepeat = NotSet; + private int _maxRepeat = NotSet; + private SrgsElementList _elements; + } + + #endregion + } +} diff --git a/src/libraries/System.Speech/src/Recognition/SrgsGrammar/SrgsItemList.cs b/src/libraries/System.Speech/src/Recognition/SrgsGrammar/SrgsItemList.cs new file mode 100644 index 00000000000000..86d75c6fc8d49a --- /dev/null +++ b/src/libraries/System.Speech/src/Recognition/SrgsGrammar/SrgsItemList.cs @@ -0,0 +1,23 @@ +// 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.Speech.Internal; + +namespace System.Speech.Recognition.SrgsGrammar +{ + [Serializable] + internal class SrgsItemList : Collection + { + #region Interfaces Implementations + + protected override void InsertItem(int index, SrgsItem item) + { + Helpers.ThrowIfNull(item, nameof(item)); + + base.InsertItem(index, item); + } + + #endregion + } +} diff --git a/src/libraries/System.Speech/src/Recognition/SrgsGrammar/SrgsNameValueTag.cs b/src/libraries/System.Speech/src/Recognition/SrgsGrammar/SrgsNameValueTag.cs new file mode 100644 index 00000000000000..cb134eb1fe614b --- /dev/null +++ b/src/libraries/System.Speech/src/Recognition/SrgsGrammar/SrgsNameValueTag.cs @@ -0,0 +1,197 @@ +// 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; +using System.Globalization; +using System.Speech.Internal; +using System.Speech.Internal.SrgsParser; +using System.Text; +using System.Xml; + +namespace System.Speech.Recognition.SrgsGrammar +{ + [Serializable] + [DebuggerDisplay("{DebuggerDisplayString ()}")] + public class SrgsNameValueTag : SrgsElement, IPropertyTag + { + #region Constructors + public SrgsNameValueTag() + { + } + public SrgsNameValueTag(object value) + { + Helpers.ThrowIfNull(value, nameof(value)); + + Value = value; + } + public SrgsNameValueTag(string name, object value) + : this(value) + { + _name = GetTrimmedName(name, "name"); + } + + #endregion + + #region public Properties + // Name of semantic property contained inside the element. + public string Name + { + get + { + return _name; + } + set + { + _name = GetTrimmedName(value, "value"); + } + } + + // Prefast cannot figure out that parameter checking is done +#pragma warning disable 56526 + // Value of semantic property contained inside the element. + public object Value + { + get { return _value; } + set + { + Helpers.ThrowIfNull(value, nameof(value)); + + if ((value is string) || + (value is bool) || + (value is int) || + (value is double)) + { + _value = value; + } + else + { + throw new ArgumentException(SR.Get(SRID.InvalidValueType), nameof(value)); + } + } + } + +#pragma warning restore 56526 + + #endregion + + #region Internal methods + + internal override void WriteSrgs(XmlWriter writer) + { + // Figure out if the tag contains a value. + bool hasValue = Value != null; + + // Do not write the tag if it is empty + bool hasName = !string.IsNullOrEmpty(_name); + // Write text + writer.WriteStartElement("tag"); + + StringBuilder sb = new(); + + if (hasName) + { + sb.Append(_name); + sb.Append('='); + } + + if (hasValue) + { + if (Value is string) + { + sb.AppendFormat(CultureInfo.InvariantCulture, "\"{0}\"", Value.ToString()); + } + else + { + sb.Append(Value.ToString()); + } + } + + writer.WriteString(sb.ToString()); + writer.WriteEndElement(); + } + + internal override void Validate(SrgsGrammar grammar) + { + SrgsTagFormat tagFormat = grammar.TagFormat; + if (tagFormat == SrgsTagFormat.Default) + { + grammar.TagFormat |= SrgsTagFormat.KeyValuePairs; + } + else if (tagFormat != SrgsTagFormat.KeyValuePairs) + { + XmlParser.ThrowSrgsException(SRID.SapiPropertiesAndSemantics); + } + } + + void IPropertyTag.NameValue(IElement parent, string name, object value) + { + _name = name; + _value = value; + } + + internal override string DebuggerDisplayString() + { + StringBuilder sb = new("SrgsNameValue "); + + if (_name != null) + { + sb.Append(_name); + sb.Append(" ("); + } + + if (_value != null) + { + if (_value is string) + { + sb.AppendFormat(CultureInfo.InvariantCulture, "\"{0}\"", _value.ToString()); + } + else + { + sb.Append(_value.ToString()); + } + } + else + { + sb.Append("null"); + } + + if (_name != null) + { + sb.Append(')'); + } + + return sb.ToString(); + } + + #endregion + + #region Private Methods + + /// + /// Checks if the name is not null and just made of blanks + /// Returned the trimmed name + /// + private static string GetTrimmedName(string name, string parameterName) + { + Helpers.ThrowIfEmptyOrNull(name, parameterName); + + // Remove the leading and trailing spaces + name = name.Trim(Helpers._achTrimChars); + + // Run again the validation code + Helpers.ThrowIfEmptyOrNull(name, parameterName); + + return name; + } + + #endregion + + #region Private Fields + + private string _name; + + private object _value; + + #endregion + } +} diff --git a/src/libraries/System.Speech/src/Recognition/SrgsGrammar/SrgsOneOf.cs b/src/libraries/System.Speech/src/Recognition/SrgsGrammar/SrgsOneOf.cs new file mode 100644 index 00000000000000..b787331ba27ab3 --- /dev/null +++ b/src/libraries/System.Speech/src/Recognition/SrgsGrammar/SrgsOneOf.cs @@ -0,0 +1,158 @@ +// 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.Diagnostics; +using System.Speech.Internal; +using System.Speech.Internal.SrgsParser; +using System.Text; +using System.Xml; + +namespace System.Speech.Recognition.SrgsGrammar +{ + [Serializable] + [DebuggerDisplay("{DebuggerDisplayString ()}")] + [DebuggerTypeProxy(typeof(OneOfDebugDisplay))] + public class SrgsOneOf : SrgsElement, IOneOf + { + #region Constructors + public SrgsOneOf() + { + } + public SrgsOneOf(params string[] items) + : this() + { + Helpers.ThrowIfNull(items, nameof(items)); + + for (int i = 0; i < items.Length; i++) + { + if (items[i] == null) + { + throw new ArgumentNullException(nameof(items), SR.Get(SRID.ParamsEntryNullIllegal)); + } + + _items.Add(new SrgsItem(items[i])); + } + } + public SrgsOneOf(params SrgsItem[] items) + : this() + { + Helpers.ThrowIfNull(items, nameof(items)); + + for (int i = 0; i < items.Length; i++) + { + SrgsItem item = items[i]; + if (item == null) + { + throw new ArgumentNullException(nameof(items), SR.Get(SRID.ParamsEntryNullIllegal)); + } + + _items.Add(item); + } + } + + #endregion + + #region public Method + public void Add(SrgsItem item) + { + Helpers.ThrowIfNull(item, nameof(item)); + + Items.Add(item); + } + + #endregion + + #region public Properties + + // ISSUE: Do we need more constructors? Take a look at RuleElementCollection.AddOneOf methods. [Bug# 37115] + public Collection Items + { + get + { + return _items; + } + } + + #endregion + + #region internal Methods + + internal override void WriteSrgs(XmlWriter writer) + { + // Write ... + writer.WriteStartElement("one-of"); + foreach (SrgsItem element in _items) + { + element.WriteSrgs(writer); + } + + writer.WriteEndElement(); + } + + internal override string DebuggerDisplayString() + { + StringBuilder sb = new("SrgsOneOf Count = "); + sb.Append(_items.Count); + return sb.ToString(); + } + + #endregion + + #region Protected Properties + + /// + /// Allows the Srgs Element base class to implement + /// features requiring recursion in the elements tree. + /// + internal override SrgsElement[] Children + { + get + { + SrgsElement[] elements = new SrgsElement[_items.Count]; + int i = 0; + foreach (SrgsItem item in _items) + { + elements[i++] = item; + } + return elements; + } + } + + #endregion + + #region Private Fields + + private SrgsItemList _items = new(); + + #endregion + + #region Private Types + + // Used by the debugger display attribute + internal class OneOfDebugDisplay + { + public OneOfDebugDisplay(SrgsOneOf oneOf) + { + _items = oneOf._items; + } + [DebuggerBrowsable(DebuggerBrowsableState.RootHidden)] + public SrgsItem[] AKeys + { + get + { + SrgsItem[] items = new SrgsItem[_items.Count]; + for (int i = 0; i < _items.Count; i++) + { + items[i] = _items[i]; + } + return items; + } + } + + private Collection _items; + } + + #endregion + } +} diff --git a/src/libraries/System.Speech/src/Recognition/SrgsGrammar/SrgsRule.cs b/src/libraries/System.Speech/src/Recognition/SrgsGrammar/SrgsRule.cs new file mode 100644 index 00000000000000..35542207f5a26f --- /dev/null +++ b/src/libraries/System.Speech/src/Recognition/SrgsGrammar/SrgsRule.cs @@ -0,0 +1,537 @@ +// 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.Diagnostics; +using System.Speech.Internal; +using System.Speech.Internal.SrgsParser; +using System.Xml; + +namespace System.Speech.Recognition.SrgsGrammar +{ + [Serializable] + [DebuggerDisplay("Rule={_id.ToString()} Scope={_scope.ToString()}")] + [DebuggerTypeProxy(typeof(SrgsRuleDebugDisplay))] + public class SrgsRule : IRule + { + #region Constructors + private SrgsRule() + { + _elements = new SrgsElementList(); + } + public SrgsRule(string id) + : this() + { + XmlParser.ValidateRuleId(id); + Id = id; + } + public SrgsRule(string id, params SrgsElement[] elements) + : this() + { + Helpers.ThrowIfNull(elements, nameof(elements)); + + XmlParser.ValidateRuleId(id); + Id = id; + + for (int iElement = 0; iElement < elements.Length; iElement++) + { + if (elements[iElement] == null) + { + throw new ArgumentNullException(nameof(elements), SR.Get(SRID.ParamsEntryNullIllegal)); + } + _elements.Add(elements[iElement]); + } + } + + #endregion + + #region public Method + public void Add(SrgsElement element) + { + Helpers.ThrowIfNull(element, nameof(element)); + + Elements.Add(element); + } + + #endregion + + #region public Properties + public Collection Elements + { + get + { + return _elements; + } + } + public string Id + { + get + { + return _id; + } + set + { + XmlParser.ValidateRuleId(value); + _id = value; + } + } + public SrgsRuleScope Scope + { + get + { + return _scope; + } + set + { + _scope = value; + _isScopeSet = true; + } + } + + /// + /// classname + /// + public string BaseClass + { + get + { + return _baseclass; + } + set + { + // base value can be null +#pragma warning disable 56526 + _baseclass = value; +#pragma warning restore 56526 + } + } + + /// + /// OnInit + /// + public string Script + { + get + { + return _script; + } + set + { + Helpers.ThrowIfEmptyOrNull(value, nameof(value)); + _script = value; + } + } + + /// + /// OnInit + /// + public string OnInit + { + get + { + return _onInit; + } + set + { + ValidateIdentifier(value); + _onInit = value; + } + } + + /// + /// OnParse + /// + public string OnParse + { + get + { + return _onParse; + } + set + { + ValidateIdentifier(value); + _onParse = value; + } + } + + /// + /// OnError + /// + public string OnError + { + get + { + return _onError; + } + set + { + ValidateIdentifier(value); + _onError = value; + } + } + + /// + /// OnRecognition + /// + public string OnRecognition + { + get + { + return _onRecognition; + } + set + { + ValidateIdentifier(value); + _onRecognition = value; + } + } + + #endregion + + #region Internal Methods + + internal void WriteSrgs(XmlWriter writer) + { + // Empty rule are not allowed + if (Elements.Count == 0) + { + XmlParser.ThrowSrgsException(SRID.InvalidEmptyRule, "rule", _id); + } + + // Write + writer.WriteStartElement("rule"); + writer.WriteAttributeString("id", _id); + if (_isScopeSet) + { + switch (_scope) + { + case SrgsRuleScope.Private: + writer.WriteAttributeString("scope", "private"); + break; + + case SrgsRuleScope.Public: + writer.WriteAttributeString("scope", "public"); + break; + } + } + // Write the 'baseclass' attribute + if (_baseclass != null) + { + writer.WriteAttributeString("sapi", "baseclass", XmlParser.sapiNamespace, _baseclass); + } + // Write + if (_dynamic != RuleDynamic.NotSet) + { + writer.WriteAttributeString("sapi", "dynamic", XmlParser.sapiNamespace, _dynamic == RuleDynamic.True ? "true" : "false"); + } + + // Write the 'onInit' code snippet + if (OnInit != null) + { + writer.WriteAttributeString("sapi", "onInit", XmlParser.sapiNamespace, OnInit); + } + + // Write + if (OnParse != null) + { + writer.WriteAttributeString("sapi", "onParse", XmlParser.sapiNamespace, OnParse); + } + + // Write + if (OnError != null) + { + writer.WriteAttributeString("sapi", "onError", XmlParser.sapiNamespace, OnError); + } + + // Write + if (OnRecognition != null) + { + writer.WriteAttributeString("sapi", "onRecognition", XmlParser.sapiNamespace, OnRecognition); + } + // Write body and footer. + Type previousElementType = null; + + foreach (SrgsElement element in _elements) + { + // Insert space between consecutive SrgsText elements. + Type elementType = element.GetType(); + + if ((elementType == typeof(SrgsText)) && (elementType == previousElementType)) + { + writer.WriteString(" "); + } + + previousElementType = elementType; + element.WriteSrgs(writer); + } + + writer.WriteEndElement(); + + // Write the