Author Topic: Change Speech Recognition Profile v2.4.0  (Read 18199 times)

Exergist

  • Global Moderator
  • Sr. Member
  • *****
  • Posts: 405
  • Ride the lightning
Change Speech Recognition Profile v2.4.0
« 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. If any revisions are made in the future, this post's content and attachments will be updated accordingly.

Comprehensive information (readme), source, previous versions, etc. for Change WSR Profile may be found on GitHub at https://github.com/Exergist/VA.Change-WSR-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 the following functionality:
  • Retrieve WSR profile info (active profile, list of available profiles)
  • Advance to the next available WSR profile
  • Activate a particular WSR profile based on its name
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. The active VoiceAttack profile is then reset so VoiceAttack recognizes the WSR profile change. 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 for performing WSR profile actions:
Referenced Assemblies: System.dll;System.Core.dll
Code: [Select]
// C# 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

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
string[] ProfileNameList; // Initialize string array for containing WSR profile names
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
ProfileNameList = new string[ProfileIdList.Length]; // Size ProfileNameList array based on size of ProfileIdList
}
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
int ActivatedProfileIndex = 0; // Variable for storing index of the desired profile

for (int i = 0; i < ProfileIdList.Length; 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.Length - 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
ProfileNameList[i] = WSRProfileName; // Add WSRProfileName to the ProfileNameList

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

if (ActivatedProfileName == "NextWSRProfile") // Check if Activated Profile Name equals "NextWSRProfile"
{
if (i + 1 != ProfileIdList.Length) // Check if the current index is NOT the last index of the ProfileIdList
ActivatedProfileIndex = i + 1; // Redefine ActivatedProfileIndex
}
}
}
VA.SetText("~~WSRProfileNames", SortDelimitedString(ProfileNameString, ';')); // Pass the list of available profile names (sorted alphabetically) back to VoiceAttack as a text variable

if (ActivatedProfileName == "GetWSRProfileInfo") // Check if user only wants to retrieve WSR profile information (WSR profile change not requested)
return; // Stop processing inline function

#endregion
#endregion

#region PERFORM THE REQUESTED WSR PROFILE CHANGE
try // Attempt the following code...
{
// Redefine ActivatedProfileName to ensure proper text formatting and further processing (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
}

string ActivatedProfileId = null; // Initialize string variable for storing the ActivatedProfileId
if (ActivatedProfileName != "NextWSRProfile") // Check if ActivatedProfileName is NOT "NextWSRProfile"
{
for (int i = 0; i < ProfileNameList.Length; i++) // Loop through each index in ProfileNameList
{
if (ActivatedProfileName == ProfileNameList[i]) // Check if ActivatedProfileName equals the ProfileNameList at index i
ActivatedProfileId = ProfileIdList[i]; // Set the ActivatedProfileId using the ProfileIdList and index i
}
if (ActivatedProfileId == null) // Check if ActivatedProfileId is null (wasn't changed)
{
ChangeResult = "Requested profile not found. Profile was not changed."; // Store result information for case where requested profile is not an available option
goto OutputSection; // Jump to code line containing the corresponding label
}
}
else
{
ActivatedProfileId = ProfileIdList[ActivatedProfileIndex]; // Set the ActivatedProfileId using the ProfileIdList and the ActivatedProfileIndex
ActivatedProfileName = ProfileNameList[ActivatedProfileIndex]; // Set the ActivatedProfileName using the ProfileNameList and the ActivatedProfileIndex
VA.SetText(">>WSRActivatedProfile", ActivatedProfileName); // Send the ActivatedProfileName back to VoiceAttack as a text variable
}

if (ActivatedProfileName == CurrentProfileName) // Check if requested WSR profile matches the current WSR profile
ChangeResult = "Requested profile 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)
{
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
}
#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

NOTE: 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.

For the sake of transparency here is the C# inline function code for searching for the presence of the Speech Properties window:
Referenced Assemblies: System.dll; System.Management.dll
Code: [Select]
// C# inline function for searching active processes for the presence of the Speech Properties window

using System;
using System.Management;

public class VAInline
{
public void main()
{
using (ManagementClass mngmtClass = new ManagementClass("Win32_Process")) // Create new ManagementClass instance
{
foreach (ManagementObject o in mngmtClass.GetInstances()) // Loop through processes
{
if (o["Name"].Equals("rundll32.exe")) // rundll32.exe is where the Windows Speech Recognition Properties functionality resides
{
// The Speech Properties window is associated with rundll32.exe Command Line "C:\Windows\System32\Speech\SpeechUX\sapi.cpl speech"
if (o["CommandLine"].ToString().Contains(@"SpeechUX\sapi.cpl") == true)
{
VA.SetBoolean("~~SpeechPropStatus", true); // Set VoiceAttack boolean variable
return; // Stop further processing of the code
}
}
}
}
}
}

// References:
// https://social.msdn.microsoft.com/Forums/en-US/669eeaeb-e6fa-403b-86fd-302b24c569fb/how-to-get-the-command-line-arguments-of-running-processes?forum=netfxbcl


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! :)



3/1/19 UPDATE:
  • Revised original post to contain all important content for the Change WSR Profile profile
  • Removed later posts that contained superseded content and profile versions
  • Released v2.4.0, which contains:
    • Trimmed down function count
    • Small improvements to variable scoping
    • Further comment cleanup



Support This and Future Efforts
If you find this profile useful, please consider buying me a cup of coffee. Thank you for your support!
 
« Last Edit: June 02, 2020, 09:38:40 PM by Exergist »