3 years passed after the original question was asked and API seems to contains the same issue, so here is a more "deep dive" answer.
TL;DR; Code example - at the bottom
The issue with the voice list is a weird design of the Microsoft Speech API - there are two sets of voices in Windows registered at different locations in registry - one is at HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Speech\Voices, another one - at HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Speech_OneCore\Voices.
The problem is that SpeechSynthesizer's (or more specifically - VoiceSynthesis's) initialization routine is nailed to the first one, meanwhile we usually need a combination of both.
So, there are actually two ways to overcome the behavior.
Option 1 (the one mentioned throughout other answers): manipulate the registry to physically copy the voice definition records from Speech_OneCore registry which make them visible to SpeechSynthesizer. Here you have plenty of options: manual registry manipulation, PowerShell script, code-based etc.
Option 2 (the one I used in my project): use reflection to put additional voices into the internal VoiceSyntesis's _installedVoices field, effectively simulating what Microsoft did in their code.
Good news are that the Speech API source code is open now, so we don't have to fumble in darkness trying to understand what we need to do.
Here's the original code snippet:
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)));
}
}
}
}
We just need to replace SAPICategories.Voices constant with another registry entry path and repeat the whole recipe.
Bad news are that all the needed classes, methods and fields used here are internal so we'll have to extensively use reflection to instantiate classes, call methods and get/set fields.
Please find below the example of my implementation - you call the InjectOneCoreVoices extension method on the synthesizer and it does the job. Note, that it throws exception if something goes wrong so don't forget proper try/catch surroundings.
public static class SpeechApiReflectionHelper
{
private const string PROP_VOICE_SYNTHESIZER = "VoiceSynthesizer";
private const string FIELD_INSTALLED_VOICES = "_installedVoices";
private const string ONE_CORE_VOICES_REGISTRY = @"HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Speech_OneCore\Voices";
private static readonly Type ObjectTokenCategoryType = typeof(SpeechSynthesizer).Assembly
.GetType("System.Speech.Internal.ObjectTokens.ObjectTokenCategory")!;
private static readonly Type VoiceInfoType = typeof(SpeechSynthesizer).Assembly
.GetType("System.Speech.Synthesis.VoiceInfo")!;
private static readonly Type InstalledVoiceType = typeof(SpeechSynthesizer).Assembly
.GetType("System.Speech.Synthesis.InstalledVoice")!;
public static void InjectOneCoreVoices(this SpeechSynthesizer synthesizer)
{
var voiceSynthesizer = GetProperty(synthesizer, PROP_VOICE_SYNTHESIZER);
if (voiceSynthesizer == null) throw new NotSupportedException($"Property not found: {PROP_VOICE_SYNTHESIZER}");
var installedVoices = GetField(voiceSynthesizer, FIELD_INSTALLED_VOICES) as IList;
if (installedVoices == null)
throw new NotSupportedException($"Field not found or null: {FIELD_INSTALLED_VOICES}");
if (ObjectTokenCategoryType
.GetMethod("Create", BindingFlags.Static | BindingFlags.NonPublic)?
.Invoke(null, new object?[] {ONE_CORE_VOICES_REGISTRY}) is not IDisposable otc)
throw new NotSupportedException($"Failed to call Create on {ObjectTokenCategoryType} instance");
using (otc)
{
if (ObjectTokenCategoryType
.GetMethod("FindMatchingTokens", BindingFlags.Instance | BindingFlags.NonPublic)?
.Invoke(otc, new object?[] {null, null}) is not IList tokens)
throw new NotSupportedException($"Failed to list matching tokens");
foreach (var token in tokens)
{
if (token == null || GetProperty(token, "Attributes") == null) continue;
var voiceInfo =
typeof(SpeechSynthesizer).Assembly
.CreateInstance(VoiceInfoType.FullName!, true,
BindingFlags.Instance | BindingFlags.NonPublic, null,
new object[] {token}, null, null);
if (voiceInfo == null)
throw new NotSupportedException($"Failed to instantiate {VoiceInfoType}");
var installedVoice =
typeof(SpeechSynthesizer).Assembly
.CreateInstance(InstalledVoiceType.FullName!, true,
BindingFlags.Instance | BindingFlags.NonPublic, null,
new object[] {voiceSynthesizer, voiceInfo}, null, null);
if (installedVoice == null)
throw new NotSupportedException($"Failed to instantiate {InstalledVoiceType}");
installedVoices.Add(installedVoice);
}
}
}
private static object? GetProperty(object target, string propName)
{
return target.GetType().GetProperty(propName, BindingFlags.Instance | BindingFlags.NonPublic)?.GetValue(target);
}
private static object? GetField(object target, string propName)
{
return target.GetType().GetField(propName, BindingFlags.Instance | BindingFlags.NonPublic)?.GetValue(target);
}
}