SpeechSynthesizer doesn't get all installed voices 3
Asked Answered
V

5

10

I have added many voices using "Add language" under region and language. These appear under Text-to-speech in Speech. (I am using Windows 10)

I want to use these in my app with the SpeechSynthesizer class in System.Speech.Synthesis.

When listing the available voices in my application only a handful of those actually available are shown:

static void Main()
{
    SpeechSynthesizer speech = new SpeechSynthesizer();

    ReadOnlyCollection<InstalledVoice> voices = speech.GetInstalledVoices();
    if (File.Exists("available_voices.txt"))
    {
        File.WriteAllText("available_voices.txt", string.Empty);
    }
    using (StreamWriter sw = File.AppendText("available_voices.txt"))
    {
        foreach (InstalledVoice voice in voices)
        {                 
            sw.WriteLine(voice.VoiceInfo.Name);                           
        }
    }
}

Looking in available_voices.txt only these voices are listed:

Microsoft David Desktop
Microsoft Hazel Desktop
Microsoft Zira Desktop
Microsoft Irina Desktop

But looking under Text-to-speech in the setttings there are many more, like Microsoft George and Microsoft Mark.

The accepted answer here: SpeechSynthesizer doesn't get all installed voices suggest changing the platform to x86. I tried this but i am not seeing any change.

This answer: SpeechSynthesizer doesn't get all installed voices 2 suggest using .NET v4.5 because of a bug in System.Speech.Synthesis. I targeted .NET Framework 4.5 but i can still only retrieve 4 voices.

None of the answers in the questions i linked helped me solve my problem, so i am asking again. Any help is appretiated.

Vespine answered 12/8, 2018 at 18:54 Comment(0)
V
2

I solved it by installing voices from another source and getting Microsoft Speech Platform - Runtime (Version 11)

The available voices can be found on microsofts website (click on the red download button and the voices should be listed)

Vespine answered 17/8, 2018 at 22:35 Comment(3)
Um, this link is on Microsoft Speech Platform - Runtime Languages (Version 11) . I don't see languages to install there. And that thing already installed on my PC.Illeetvilaine
@Kosmos Sorry for the late answed but if you press download there is a list of the available languages. (Edited answer)Vespine
Unfortunately, the link contains only some languages, not all. E.g., I cannot find Greek in the list.Kaete
B
12

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);
    }
}
Breazeale answered 20/2, 2022 at 19:54 Comment(0)
D
6

After trying about all published solutions, I solved it by editing the registry:
copying Computer\HKEY_LOCAL_MACHINE\SOFTWARE\WOW6432Node\Microsoft\Speech_OneCore\Voices\Tokens\MSTTS_V110_heIL_Asaf (where MSTTS_V110_heIL_Asaf is the registry folder of the voice I want to use in .NET, but don't appear in GetInstalledVoices()) to a registry address that looks the same but instead of Speech_OneCore it is just Speech.

technically, to copy the registry folder, i exported the original folder, then edited the .reg file to change Speech OneCore to Speech, and then applied that new .reg file.

Diadem answered 19/7, 2020 at 11:59 Comment(0)
V
2

I solved it by installing voices from another source and getting Microsoft Speech Platform - Runtime (Version 11)

The available voices can be found on microsofts website (click on the red download button and the voices should be listed)

Vespine answered 17/8, 2018 at 22:35 Comment(3)
Um, this link is on Microsoft Speech Platform - Runtime Languages (Version 11) . I don't see languages to install there. And that thing already installed on my PC.Illeetvilaine
@Kosmos Sorry for the late answed but if you press download there is a list of the available languages. (Edited answer)Vespine
Unfortunately, the link contains only some languages, not all. E.g., I cannot find Greek in the list.Kaete
O
2

Sorry if my answer comes so late after the subject was posted but I developed a small tool which allows to patch the installed voices to make them available for the .NET text-to-speech engine.

The tool copies selected items from the "HKLM\SOFTWARE\Microsoft\Speech_OneCore\Voices\Tokens" key to "HKLM\SOFTWARE\Microsoft\Speech\Voices\Tokens".

If you're interested : TTSVoicePatcher (it's freeware, FR/EN)

Due to the manipulation of keys in HKLM, the tool requires administrator rights to be launched.

Obliquely answered 21/3, 2022 at 8:12 Comment(0)
A
0

The Microsoft Speech Platform - Runtime Languages (Version 11) on the Microsoft website seem to contain only languages that are already installed. Not the ones that can be found under Speech_OneCore.

Ascend answered 29/1, 2022 at 13:34 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.