Author Topic: Change Speech Recognition Profile  (Read 1587 times)

Exergist

  • Moderator
  • Sr. Member
  • *****
  • Posts: 300
  • Can you dig it?
Change Speech Recognition Profile
« on: December 28, 2017, 08:53:03 am »
The attached VoiceAttack Profile provides the means for changing the Windows Speech Recognition (WSR) profile within the VoiceAttack environment. A summary of why WSR profile changing is valuable as well as the profile functionality is provided below.

Comprehensive information (readme), source, etc. for Change WSR Profile may be found on GitHub at https://github.com/Exergist/VA.Change-WSR-Profile. If desired, you can follow the "Releases" link at the top of the main page to access all versions of the profile.



To effectively use VoiceAttack (with the default speech engine) you need to train the speech engine to properly recognize your voice. A WSR profile contains the information from the voice training. Many folks will only use one WSR profile, however if you have multiple people using VoiceAttack on the same PC you will definitely want to employ multiple corresponding WSR profiles. In addition, if you have more than one microphone configuration or even different ambient noise environments you may also benefit (i.e., have better recognition accuracy) from multiple WSR profiles. The catch is that changing WSR profiles through the Windows interface (via the "Speech Properties" panel) is cumbersome. This is where the VoiceAttack profile Change WSR Profile comes in handy.

Change WSR Profile contains two primary commands that initiate changes to the WSR profile via voice command or via a text variable. These then call on another command that contains a C# inline function that performs Windows registry queries and edits to obtain WSR profile data and change the WSR profile based on your provided input. So for instance if you have multiple people using a VR headset or want to change to a different audio configuration for Let's Play vs. Solo you can now make the corresponding speech recognition profile switches quickly within the VoiceAttack environment. Again, if you want more details please check out the readme at the above GitHub page.

Here is the C# inline function code (see the comments at the top for the Referenced Assemblies):
Code: [Select]
// Inline function for retrieving Windows Speech Recognition (WSR) profile data from the Windows registry and attempting to change the WSR profile based on user input
// Referenced Assemblies: System.dll;System.Core.dll

using System;
using System.Linq;
using Microsoft.Win32;

public class VAInline
{
public void main()
{
#region OBTAIN NAME OF REQUESTED WSR PROFILE
string ActivatedProfileName = VA.GetText(">>WSRActivatedProfile"); // Store profile name that user wants to activate, passed from VoiceAttack
#endregion

#region RETRIEVE WSR PROFILE DATA FROM WINDOWS REGISTRY
#region Perform registry queries for current WSR profile
string RecoProfilesRegPath = @"Software\Microsoft\Speech\RecoProfiles"; // Variable for storing portion of the registry path where the Windows Speech Recognition profile data is stored
string CurrentProfileData = ""; // Variable that will store profile data from the registry
string CurrentProfileId = ""; // Variable that will store profile ID info from the registry
string CurrentProfileName = ""; // Variable that will store the name of the currently activated profile
string ProfileNameString = ""; // Variable that will store the names of all available profiles
string ProfileChangeErrorDetail = ""; // Variable that will store error information (if applicable)
string ChangeResult = ""; // String that will hold the result information from the (attempted) WSR profile change

try // Attempt the following code...
{
using (RegistryKey RecoProfiles = Registry.CurrentUser.OpenSubKey(RecoProfilesRegPath)) // Capture registry information for the key at the specified registry path (read-only). "using" also properly disposes RegistryKey
{
if (RecoProfiles != null) // Check if key is not null
{
CurrentProfileData = (string)RecoProfiles.GetValue("DefaultTokenId"); // Extract the value data associated with the "DefaultTokenId" value name, which corresponds to the ID for the current WSR profile
CurrentProfileId = CurrentProfileData.Substring(CurrentProfileData.IndexOf("{")).Replace("{", "").Replace("}", ""); // Extract the current WSR profile ID from the profile data and remove the bracket characters
}
else // key is null
{
ProfileChangeErrorDetail = "Error during WSR profile data retrieval: RecoProfiles key is null."; // Store error detail
goto OutputSection; // Jump to code line containing the corresponding label
}
}
}
catch // Perform if "try" encounter an exception (error)
{
ProfileChangeErrorDetail = "General error during current WSR profile data retrieval."; // Store error detail
goto OutputSection; // Jump to code line containing the corresponding label
}
#endregion

#region Perform registry queries for all available WSR profiles
string TokensRegPath = RecoProfilesRegPath + @"\Tokens"; // Store registry path of the Tokens folder inside of RecoProfiles
string[] ProfileIdList; // Initialize string array for containing WSR profile IDs
try // Attempt the following code...
{
using (RegistryKey Tokens = Registry.CurrentUser.OpenSubKey(TokensRegPath)) // Capture registry information for the key at the specified registry path (read-only). "using" also properly disposes RegistryKey
{
if (Tokens != null) // Check if key is not null
ProfileIdList = Tokens.GetSubKeyNames(); // Extract list of WSR profile IDs corresponding to all WSR profiles
else
{
ProfileChangeErrorDetail = "Error during WSR profile data retrieval: Tokens key is null."; // Store error detail
goto OutputSection; // Jump to code line containing the corresponding label
}
}
}
catch // Perform if "try" encounter an exception (error)
{
ProfileChangeErrorDetail = "General error during WSR profile data retrieval for all available profiles."; // Store error detail
goto OutputSection; // Jump to code line containing the corresponding label
}
#endregion

#region Identify names of current, requested (activated), and all available WSR profiles
string ProfileIdString = ""; // Variable that will store the registry IDs of all available profiles
string WSRProfileName = ""; // Variable for storing profile name data

for (int i = 0; i < ProfileIdList.Count(); i++) // Loop through all profile IDs contained within ProfileIdList
{
ProfileIdList[i] = ProfileIdList[i].Replace("{", "").Replace("}", ""); // Remove bracket characters from ProfileIdList entries. Brackets stored in text variables appear to give VoiceAttack issues.
ProfileIdString += ProfileIdList[i] + "; "; // Build a string of available WSR profile names inside a single variable
try // Attempt the following code...
{
string ProfileKeyPath = TokensRegPath + @"\" + "{" + ProfileIdList[i] + "}"; // Define registry path to individual profile key
WSRProfileName = Registry.CurrentUser.OpenSubKey(ProfileKeyPath).GetValue(null).ToString(); // Get actual WSR profile name associated with ProfileIdList[i] from registry (read-only)
}
catch // Perform if "try" encounter an exception (error)
{
ProfileChangeErrorDetail = "Error during WSR profile data retrieval: Issue obtaining WSRProfileName."; // Store error detail
goto OutputSection; // Jump to code line containing the corresponding label
}

if (i < ProfileIdList.Count() - 1) // Check if counter is less than the total number of profile IDs
ProfileNameString += WSRProfileName + "; "; // Build a list of WSR profile names inside a single string variable delimited by ";"
else // Counter is equal to the total number of profile IDs
ProfileNameString += WSRProfileName; // Complete the list of WSR profile names inside a single string variable

if (ProfileIdList[i] == CurrentProfileId) // Check if value stored in ProfileIdList[i] matches the CurrentProfileId
{
CurrentProfileName = WSRProfileName; // Store the WSR CurrentProfileName
VA.SetText("~~WSRCurrentProfile", CurrentProfileName); // Pass the CurrentProfileName back to VoiceAttack as a text variable
}
}
VA.SetText("~~WSRProfileNames", SortDelimitedString(ProfileNameString, ';')); // Pass the list of available profile names (sorted alphabetically) back to VoiceAttack as a text variable
#endregion
#endregion

#region PERFORM THE REQUESTED WSR PROFILE CHANGE
try // Attempt the following code...
{
// Redefine ActivatedProfileName to ensure proper further processing and text formatting
// Also provides means to check for ActivatedProfileName within the list of available WSR profiles
ActivatedProfileName = ProfileNameString.Substring(ProfileNameString.IndexOf(ActivatedProfileName, StringComparison.OrdinalIgnoreCase), ActivatedProfileName.Length);
VA.SetText(">>WSRActivatedProfile", ActivatedProfileName); // Store (correctly formatted) activated profile name in VoiceAttack text variable
}
catch // Perform if "try" encounter an exception (error)
{
// Empty catch statement
}
int ActivatedProfileCharIndex = ProfileNameString.IndexOf(ActivatedProfileName, StringComparison.OrdinalIgnoreCase); // Get character index of requested WSR profile from within the list (string) of available profiles (case insensitive)
if (ActivatedProfileCharIndex >= 0) // Check if requested WSR profile is an available option
{
int ActivatedProfileNameIndex = ProfileNameString.Substring(0, ActivatedProfileCharIndex).Split(';').Length; // Store index of ActivatedProfileName within the ProfileNameString

if (ActivatedProfileName == CurrentProfileName) // Check if requested WSR profile matches the current WSR profile
ChangeResult = "Requested profile (" + ActivatedProfileName + ") already activated"; // Store result information for case where requested profile is the current profile
else // Requested profile is not the same as the current profile (so a profile change may be possible)
{
int ProfileIdCharIndex; // Integer that will store the starting character index of the desired profile ID inside of the ProfileIdString
if (ActivatedProfileNameIndex == 1) // Check if ActivatedProfileNameIndex = 1
ProfileIdCharIndex = 0; // Set the ProfileIdCharIndex to 0
else // ActivatedProfileNameIndex is not 1
{
ProfileIdCharIndex = ProfileIdString.IndexOf(";"); // Get character index of first ";" in ProfileIdString
for (int i = 2; i < ActivatedProfileNameIndex; i++) // Loop based on the ActivatedProfileNameIndex
{
ProfileIdCharIndex = ProfileIdString.IndexOf(";", ProfileIdCharIndex + 1); // Redefine ProfileIdCharIndex to identify character index of desired profile ID within ProfileIdString
}
ProfileIdCharIndex += 2; // Add two to the index to account for the "space" and ";" characters
}
string ActivatedProfileId = ProfileIdString.Substring(ProfileIdCharIndex, ProfileIdString.IndexOf(';')); // Extract desired profile ID from ProfileIdString using the found ProfileIdCharIndex and store it

try // Attempt the following code...
{
using (RegistryKey WSRProfileRoot = Registry.CurrentUser.OpenSubKey(RecoProfilesRegPath, true)) // Capture registry information for the key at the specified registry path. "using" also properly disposes RegistryKey
{
string ActivatedProfileDataValue = @"HKEY_CURRENT_USER\" + RecoProfilesRegPath + @"\Tokens\" + "{" + ActivatedProfileId + "}"; // Define the registry data string corresponding to the ActivatedProfileName
WSRProfileRoot.SetValue("DefaultTokenId", ActivatedProfileDataValue); // Change the data value of the "DefaultTokenId" entry (inside the RecoProfiles key) in the Windows registry to ActivatedProfileDataValue, which will change the WSR profile to ActivatedProfileName
}
}
catch // Perform if "try" encounter an exception (error)
{
ProfileChangeErrorDetail = "General error during WSR profile change."; // Store error detail
goto OutputSection; // Jump to code line containing the corresponding label
}

ChangeResult = "Activated Profile = " + ActivatedProfileName; // Store result information for case where requested profile is not the same as the current profile
}
}
else // Requested WSR profile is not an available option
ChangeResult = "Requested profile (" + ActivatedProfileName + ") not found. Profile was not changed."; // Store result information for case where requested profile is not an available option
#endregion

#region OUTPUT INFORMATION
OutputSection: // goto marker (destination)
if (ProfileChangeErrorDetail != "") // Check if an error was encountered during processing (ProfileChangeErrorDetail would be non-blank)
{
ChangeResult = "An error occurred. " + ProfileChangeErrorDetail; // Store result information for case where an error occurred during processing
VA.SetText(">>WSRActivatedProfile", null); // Set the VoiceAttack text variable to null (not set)
}

VA.SetText("~~WSRChangeResult", ChangeResult); // Pass the ChangeResult back to VoiceAttack as a text variable
#endregion
}

#region Function for sorting a delimited string
public static string SortDelimitedString(string name, char delimiter)
{
name = name.Replace(delimiter + " ", delimiter.ToString()); // Replace delimiting "; " with delimiting ";"
string[] stringArray = name.Split(delimiter); // Split up delimited string and store each item within stringArray
Array.Sort(stringArray); // Sort the stringArray
string returnValue = ""; // Initialize string variable for storing sorted item list
for (int i = stringArray.GetLowerBound(0); i <= stringArray.GetUpperBound(0); i++) // Loop through all the items in stringArray
{
returnValue = returnValue + stringArray[i] + delimiter + " "; // Rebuild the original string that is now sorted
}
return returnValue.Remove(returnValue.Length - 2, 2); // Remove last 2 characters from returnValue before sending it back to main
}
#endregion
}

// References:
// https://social.msdn.microsoft.com/Forums/en-US/f4feb3eb-0920-4923-83e8-6f2ef5bd4217/how-i-can-read-default-value-from-registry?forum=csharplanguage
// https://stackoverflow.com/questions/8935161/how-to-add-a-case-insensitive-option-to-array-indexof
// https://stackoverflow.com/questions/444798/case-insensitive-containsstring/444818#444818
// https://stackoverflow.com/questions/541954/how-would-you-count-occurrences-of-a-string-within-a-string
// http://www.dotnetspider.com/resources/34547-Function-Sort-comma-separated-string.aspx

Two things to point out:
  • If you try to make a WSR profile switch while the Windows Speech Properties panel is open the profile change will NOT be accepted by the registry. This panel needs to be closed for the switch to work properly. You shouldn't have to worry too much about this since I've included code within the profile to check for the presence of the Speech Properties panel as well as terminate the requested command and alert the user if the panel is open.
  • VoiceAttack will automatically reset the active profile so that it can recognize the change to the WSR profile.


I've been working on this for a little while now, and I'm glad I can finally release it to the community. Special thanks goes out to:
  • Gary for providing initial feedback about the profile and some info about VoiceAttack's inner workings.
  • Antaniserse and Gangrel for providing feedback during development of the beta versions.
  • Pfeil for sharing his method for restarting VoiceAttack (for use with v2.2.0 and earlier).
Please let me know if you encounter any issues with the profile. Enjoy! :)



EDIT: VoiceAttack v1.7 introduced the ability to reset the active profile. The reset action replaces the restart action in v2.2.0 of Change WSR Profile.
EDIT: Updated VoiceAttack variables to be more appropriately scoped and updated some formatting
« Last Edit: June 14, 2018, 07:40:41 am by Exergist »

Exergist

  • Moderator
  • Sr. Member
  • *****
  • Posts: 300
  • Can you dig it?
Re: Change Speech Recognition Profile
« Reply #1 on: June 13, 2018, 12:30:10 pm »
Updated to v2.3.0 which includes general tidying and adds:
  • Command for only retrieving WSR profile information
  • Command to toggle to the next WSR profile
  • Better use of scoped variables
« Last Edit: June 14, 2018, 07:41:38 am by Exergist »