Skip to content

Commit

Permalink
Support for Windows OneCore voices included in Windows 10. (PR #7110,…
Browse files Browse the repository at this point in the history
… issue #6159)

This uses a C++/CX dll to access the UWP SpeechSynthesizer class. There are other UWP APIs we might like to access in future (e.g. OCR), so rather than making this dll specific to OneCore speech, it's called nvdaHelperLocalWin10. The build system for this dll makes it easy to add other components in future.
In addition, this required code to generate balanced XML from an NVDA speech sequence. Although we use SSML for eSpeak, eSpeak happily accepts unbalanced (malformed) XML. OneCore speech does not. This code is in the speechXml module. This might eventually be reused to replace the ugly balanced XML code in the SAPI5 driver.

Note that NVDA can no longer be built with Visual Studio 2015 Express. You must use Visual Studio 2015 Community, as you also need the Windows 10 Tools and SDK. See the updated dependencies in the readme for details.
Also, we now bundle the VC 2015 runtime, as some systems don't have it and it is needed for OneCore Speech.
  • Loading branch information
jcsteh committed Jun 13, 2017
1 parent d8f07d3 commit 2ee954f
Show file tree
Hide file tree
Showing 14 changed files with 1,138 additions and 21 deletions.
13 changes: 0 additions & 13 deletions nvdaHelper/archBuild_sconscript
Expand Up @@ -75,19 +75,6 @@ env.Append(LINKFLAGS='/OPT:REF') #having symbols usually turns this off but we h

Export('env')

import versionInfo
projectRCSubstDict={
'%version_year%':env['version_year'],
'%version_major%':env['version_major'],
'%version_minor%':env['version_minor'],
'%version_build%':env['version_build'],
'%copyright%':env['copyright'],
'%publisher%':env['publisher'],
'%version%':env['version'],
'%productName%':"%s (%s)"%(versionInfo.name,versionInfo.longName),
}
env['projectResFile']=env.RES(env.Substfile('nvda.rc.subst',SUBST_DICT=projectRCSubstDict))

acrobatAccessRPCStubs=env.SConscript('acrobatAccess_sconscript')
Export('acrobatAccessRPCStubs')
if TARGET_ARCH=='x86':
Expand Down
1 change: 1 addition & 0 deletions nvdaHelper/local/nvdaHelperLocal.def
Expand Up @@ -54,3 +54,4 @@ EXPORTS
dllImportTableHooks_hookSingle
dllImportTableHooks_unhookSingle
audioDucking_shouldDelay
logMessage
133 changes: 133 additions & 0 deletions nvdaHelper/localWin10/oneCoreSpeech.cpp
@@ -0,0 +1,133 @@
/*
Code for C dll bridge to Windows OneCore voices.
This file is a part of the NVDA project.
URL: http://www.nvaccess.org/
Copyright 2016-2017 Tyler Spivey, NV Access Limited.
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License version 2.0, as published by
the Free Software Foundation.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
This license can be found at:
http://www.gnu.org/licenses/old-licenses/gpl-2.0.html
*/

#include <string>
#include <collection.h>
#include <ppltasks.h>
#include <wrl.h>
#include <robuffer.h>
#include <common/log.h>
#include "oneCoreSpeech.h"

using namespace std;
using namespace Platform;
using namespace Windows::Media::SpeechSynthesis;
using namespace concurrency;
using namespace Windows::Storage::Streams;
using namespace Microsoft::WRL;
using namespace Windows::Media;
using namespace Windows::Foundation::Collections;

byte* getBytes(IBuffer^ buffer) {
// We want direct access to the buffer rather than copying it.
// To do this, we need to get to the IBufferByteAccess interface.
// See http://cm-bloggers.blogspot.com/2012/09/accessing-image-pixel-data-in-ccx.html
ComPtr<IInspectable> insp = reinterpret_cast<IInspectable*>(buffer);
ComPtr<IBufferByteAccess> bufferByteAccess;
if (FAILED(insp.As(&bufferByteAccess))) {
LOG_ERROR(L"Couldn't get IBufferByteAccess from IBuffer");
return nullptr;
}
byte* bytes = nullptr;
bufferByteAccess->Buffer(&bytes);
return bytes;
}

OcSpeech* __stdcall ocSpeech_initialize() {
auto instance = new OcSpeech;
instance->synth = ref new SpeechSynthesizer();
return instance;
}

void __stdcall ocSpeech_terminate(OcSpeech* instance) {
delete instance;
}

void __stdcall ocSpeech_setCallback(OcSpeech* instance, ocSpeech_Callback fn) {
instance->callback = fn;
}

void __stdcall ocSpeech_speak(OcSpeech* instance, char16 *text) {
String^ textStr = ref new String(text);
auto markersStr = make_shared<wstring>();
task<SpeechSynthesisStream ^> speakTask;
try {
speakTask = create_task(instance->synth->SynthesizeSsmlToStreamAsync(textStr));
} catch (Platform::Exception ^e) {
LOG_ERROR(L"Error " << e->HResult << L": " << e->Message->Data());
instance->callback(NULL, 0, NULL);
return;
}
speakTask.then([markersStr] (SpeechSynthesisStream^ speechStream) {
// speechStream->Size is 64 bit, but Buffer can only take 32 bit.
// We shouldn't get values above 32 bit in reality.
const unsigned int size = static_cast<unsigned int>(speechStream->Size);
Buffer^ buffer = ref new Buffer(size);
IVectorView<IMediaMarker^>^ markers = speechStream->Markers;
for (auto&& marker : markers) {
if (markersStr->length() > 0) {
*markersStr += L"|";
}
*markersStr += marker->Text->Data();
*markersStr += L":";
*markersStr += to_wstring(marker->Time.Duration);
}
auto t = create_task(speechStream->ReadAsync(buffer, size, Windows::Storage::Streams::InputStreamOptions::None));
return t;
}).then([instance, markersStr] (IBuffer^ buffer) {
// Data has been read from the speech stream.
// Pass it to the callback.
byte* bytes = getBytes(buffer);
instance->callback(bytes, buffer->Length, markersStr->c_str());
}).then([instance] (task<void> previous) {
// Catch any unhandled exceptions that occurred during these tasks.
try {
previous.get();
} catch (Platform::Exception^ e) {
LOG_ERROR(L"Error " << e->HResult << L": " << e->Message->Data());
instance->callback(NULL, 0, NULL);
}
});
}

// We use BSTR because we need the string to stay around until the caller is done with it
// but the caller then needs to free it.
// We can't just use malloc because the caller might be using a different CRT
// and calling malloc and free from different CRTs isn't safe.
BSTR __stdcall ocSpeech_getVoices(OcSpeech* instance) {
wstring voices;
for (unsigned int i = 0; i < instance->synth->AllVoices->Size; ++i) {
VoiceInformation^ info = instance->synth->AllVoices->GetAt(i);
voices += info->Id->Data();
voices += L":";
voices += info->DisplayName->Data();
if (i != instance->synth->AllVoices->Size - 1) {
voices += L"|";
}
}
return SysAllocString(voices.c_str());
}

const char16* __stdcall ocSpeech_getCurrentVoiceId(OcSpeech* instance) {
return instance->synth->Voice->Id->Data();
}

void __stdcall ocSpeech_setVoice(OcSpeech* instance, int index) {
instance->synth->Voice = instance->synth->AllVoices->GetAt(index);
}

const char16 * __stdcall ocSpeech_getCurrentVoiceLanguage(OcSpeech* instance) {
return instance->synth->Voice->Language->Data();
}
34 changes: 34 additions & 0 deletions nvdaHelper/localWin10/oneCoreSpeech.h
@@ -0,0 +1,34 @@
/*
Header for C dll bridge to Windows OneCore voices.
This file is a part of the NVDA project.
URL: http://www.nvaccess.org/
Copyright 2016-2017 Tyler Spivey, NV Access Limited.
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License version 2.0, as published by
the Free Software Foundation.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
This license can be found at:
http://www.gnu.org/licenses/old-licenses/gpl-2.0.html
*/

#pragma once
#define export __declspec(dllexport)

typedef void (*ocSpeech_Callback)(byte* data, int length, const char16* markers);
typedef struct {
Windows::Media::SpeechSynthesis::SpeechSynthesizer ^synth;
ocSpeech_Callback callback;
} OcSpeech;

extern "C" {
export OcSpeech* __stdcall ocSpeech_initialize();
export void __stdcall ocSpeech_terminate(OcSpeech* instance);
export void __stdcall ocSpeech_setCallback(OcSpeech* instance, ocSpeech_Callback fn);
export void __stdcall ocSpeech_speak(OcSpeech* instance, char16 *text);
export BSTR __stdcall ocSpeech_getVoices(OcSpeech* instance);
export const char16* __stdcall ocSpeech_getCurrentVoiceId(OcSpeech* instance);
export void __stdcall ocSpeech_setVoice(OcSpeech* instance, int index);
export const char16* __stdcall ocSpeech_getCurrentVoiceLanguage(OcSpeech* instance);
}
73 changes: 73 additions & 0 deletions nvdaHelper/localWin10/sconscript
@@ -0,0 +1,73 @@
###
#This file is a part of the NVDA project.
#URL: http://www.nvaccess.org/
#Copyright 2016-2017 NV Access Limited
#This program is free software: you can redistribute it and/or modify
#it under the terms of the GNU General Public License version 2.0, as published by
#the Free Software Foundation.
#This program is distributed in the hope that it will be useful,
#but WITHOUT ANY WARRANTY; without even the implied warranty of
#MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
#This license can be found at:
#http://www.gnu.org/licenses/old-licenses/gpl-2.0.html
###

import os

Import(
'env',
'sourceDir',
'libInstallDir',
'localLib',
)

TARGET_ARCH=env['TARGET_ARCH']
debug=env['nvdaHelperDebugFlags']
release=env['release']
signExec=env['signExec'] if env['certFile'] else None
progFilesX86 = os.getenv("ProgramFiles(x86)")

env.Append(CPPDEFINES=[
'UNICODE', '_CRT_SECURE_NO_DEPRECATE',
('LOGLEVEL','${nvdaHelperLogLevel}')])
env.Append(CCFLAGS=['/W3', '/WX'])
env.Append(CXXFLAGS=['/EHsc', '/ZW',
r'/AI%s\Microsoft Visual Studio 14.0\VC\vcpackages' % progFilesX86,
r'/AI%s\Windows Kits\10\UnionMetadata' % progFilesX86])
env.Append(CPPPATH=[Dir('..').abspath])
env.Append(LINKFLAGS=['/incremental:no', '/WX'])
env.Append(LINKFLAGS='/release') # We always want a checksum in the header

if not release:
env.Append(CCFLAGS=['/Od'])
else:
env.Append(CCFLAGS='/O2')
env.Append(CCFLAGS='/GL')
env.Append(LINKFLAGS=['/LTCG'])

if 'RTC' in debug:
env.Append(CCFLAGS=['/RTCsu'])

# We always want debug symbols
env.Append(PDB='${TARGET}.pdb')
env.Append(LINKFLAGS='/OPT:REF') #having symbols usually turns this off but we have no need for unused symbols

localWin10Lib = env.SharedLibrary(
target="nvdaHelperLocalWin10",
source=[
env['projectResFile'],
'oneCoreSpeech.cpp',
],
LIBS=["oleaut32", localLib[2]],
)
if signExec:
env.AddPostAction(localWin10Lib[0], [signExec])
env.Install(libInstallDir, localWin10Lib)

# UWP dlls can only be dynamically linked with the CRT,
# but some systems might not have this version of the CRT.
# Therefore, we must include it.
vcRedist = os.path.join(progFilesX86, r"Microsoft Visual Studio 14.0\VC\redist\onecore\x86\Microsoft.VC140.CRT")
for fn in ("msvcp140.dll", "vccorlib140.dll", "vcruntime140.dll"):
fn = os.path.join(vcRedist, fn)
env.Install(sourceDir, fn)
8 changes: 6 additions & 2 deletions readme.md
Expand Up @@ -36,8 +36,12 @@ The NVDA source depends on several other packages to run correctly.
The following dependencies need to be installed on your system:

* [Python](http://www.python.org/), version 2.7.13, 32 bit
* Microsoft Visual Studio 2015 (Express for Desktop with VC++ and Windows SDK 7.1A support, or Community with VC++ and Windows XP support):
* [Download for Visual Studio 2015 Express for Desktop](https://go.microsoft.com/fwlink/?LinkId=691984&clcid=0x409)
* Microsoft Visual Studio Community 2015 with Update 3:
* To download, you will need to [join the Visual Studio Dev Essentials program](https://my.visualstudio.com/Benefits?wt.mc_id=o~msft~vscom~devessentials-hero~30569&campaign=o~msft~vscom~devessentials-hero~30569).
After you have joined, Visual Studio Community 2015 with Update 3 will be available on the Downloads tab.
* When installing Visual Studio, you need to enable the following:
* In Visual C++: Common Tools for Visual C++ 2015, Windows XP Support for C++
* Windows and Web Development -> Universal Windows App Development Tools -> Tools (1.4.1) and Windows 10 SDK (10.0.14393)

### Git Submodules
Most of the dependencies are contained in Git submodules.
Expand Down
19 changes: 19 additions & 0 deletions sconstruct
Expand Up @@ -152,9 +152,27 @@ env64=env.Clone(TARGET_ARCH='x86_64',tools=archTools)
# Hack around odd bug where some tool [after] msvc states that static and shared objects are different
env32['STATIC_AND_SHARED_OBJECTS_ARE_THE_SAME'] = 1
env64['STATIC_AND_SHARED_OBJECTS_ARE_THE_SAME'] = 1
# Environment for functionality that only works on Windows 10.
envWin10 = env.Clone(TARGET_ARCH='x86', tools=['default'])

env=env32

projectRCSubstDict={
'%version_year%':env['version_year'],
'%version_major%':env['version_major'],
'%version_minor%':env['version_minor'],
'%version_build%':env['version_build'],
'%copyright%':env['copyright'],
'%publisher%':env['publisher'],
'%version%':env['version'],
'%productName%':"%s (%s)"%(versionInfo.name,versionInfo.longName),
}
resFile=env.RES(target='build/nvda.res',
source=env.Substfile(target='build/nvda.rc', source='nvdaHelper/nvda.rc.subst', SUBST_DICT=projectRCSubstDict))
env32['projectResFile'] = resFile
env64['projectResFile'] = resFile
envWin10['projectResFile'] = resFile

#Fill sourceDir with anything provided for it by miscDeps
env.recursiveCopy(sourceDir,Dir('miscdeps/source'))

Expand All @@ -163,6 +181,7 @@ env.SConscript('source/comInterfaces_sconscript',exports=['env'])
#Process nvdaHelper scons files
env32.SConscript('nvdaHelper/archBuild_sconscript',exports={'env':env32,'clientInstallDir':clientDir.Dir('x86'),'libInstallDir':sourceLibDir},variant_dir='build/x86')
env64.SConscript('nvdaHelper/archBuild_sconscript',exports={'env':env64,'clientInstallDir':clientDir.Dir('x64'),'libInstallDir':sourceLibDir64},variant_dir='build/x86_64')
envWin10.SConscript('nvdaHelper/localWin10/sconscript', exports={'env': envWin10, 'libInstallDir': sourceLibDir}, variant_dir='build/x86/localWin10')

#Allow all NVDA's gettext po files to be compiled in source/locale
for po in env.Glob(sourceDir.path+'/locale/*/lc_messages/*.po'):
Expand Down
19 changes: 14 additions & 5 deletions source/languageHandler.py
Expand Up @@ -14,21 +14,32 @@
#a few Windows locale constants
LOCALE_SLANGUAGE=0x2
LOCALE_SLANGDISPLAYNAME=0x6f
LOCALE_CUSTOM_UNSPECIFIED = 0x1000
#: Returned from L{localeNameToWindowsLCID} when the locale name cannot be mapped to a locale identifier.
#: This might be because Windows doesn't know about the locale (e.g. "an"),
#: because it is not a standardized locale name anywhere (e.g. "zz")
#: or because it is not a legal locale name (e.g. "zzzz").
LCID_NONE = 0 # 0 used instead of None for backwards compatibility.

curLang="en"

def localeNameToWindowsLCID(localeName):
"""Retreave the Windows locale identifier (LCID) for the given locale name
@param localeName: a string of 2letterLanguage_2letterCountry or or just 2letterLanguage
@type localeName: string
@returns: a Windows LCID
@returns: a Windows LCID or L{LCID_NONE} if it could not be retrieved.
@rtype: integer
"""
#Windows Vista is able to convert locale names to LCIDs
func_LocaleNameToLCID=getattr(ctypes.windll.kernel32,'LocaleNameToLCID',None)
if func_LocaleNameToLCID is not None:
localeName=localeName.replace('_','-')
LCID=func_LocaleNameToLCID(unicode(localeName),0)
# #6259: In Windows 10, LOCALE_CUSTOM_UNSPECIFIED is returned for any locale name unknown to Windows.
# This was observed for Aragonese ("an").
# See https://msdn.microsoft.com/en-us/library/system.globalization.cultureinfo.lcid(v=vs.110).aspx.
if LCID==LOCALE_CUSTOM_UNSPECIFIED:
LCID=LCID_NONE
else: #Windows doesn't have this functionality, manually search Python's windows_locale dictionary for the LCID
localeName=locale.normalize(localeName)
if '.' in localeName:
Expand All @@ -37,7 +48,7 @@ def localeNameToWindowsLCID(localeName):
if len(LCList)>0:
LCID=LCList[0]
else:
LCID=0
LCID=LCID_NONE
return LCID

def windowsLCIDToLocaleName(lcid):
Expand All @@ -57,9 +68,7 @@ def getLanguageDescription(language):
"""Finds out the description (localized full name) of a given local name"""
desc=None
LCID=localeNameToWindowsLCID(language)
# #6259: LCID 0x1000 denotes custom locale in Windows 10, thus returns "unknown language" or an odd description (observed for Aragonese).
# See https://msdn.microsoft.com/en-us/library/system.globalization.cultureinfo.lcid(v=vs.110).aspx.
if LCID not in (0, 0x1000):
if LCID is not LCID_NONE:
buf=ctypes.create_unicode_buffer(1024)
#If the original locale didn't have country info (was just language) then make sure we just get language from Windows
if '_' not in language:
Expand Down
2 changes: 1 addition & 1 deletion source/speech.py
Expand Up @@ -1658,7 +1658,7 @@ def __init__(self,index):
def __repr__(self):
return "IndexCommand(%r)" % self.index

class CharacterModeCommand(object):
class CharacterModeCommand(SpeechCommand):
"""Turns character mode on and off for speech synths."""

def __init__(self,state):
Expand Down

0 comments on commit 2ee954f

Please sign in to comment.