Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Update NVDA Controller client to support speaking SSML sequences #15734

Merged
merged 27 commits into from
Nov 23, 2023
Merged
Show file tree
Hide file tree
Changes from 25 commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
1b0ff0a
Add support to speak SSMl to the NVDA controller client
LeonarddeR Oct 26, 2023
6a873da
Small fixups
LeonarddeR Nov 2, 2023
a35099d
Apply suggestions from code review
LeonarddeR Nov 3, 2023
75abc81
Use 32 bit enums since they play better with ctypes defaults
LeonarddeR Nov 4, 2023
2f7499a
Merge remote-tracking branch 'origin/master' into nvdaControllerClient
LeonarddeR Nov 4, 2023
a773456
Use user configured symbol level for -1
LeonarddeR Nov 4, 2023
78bb6b0
Use enum for symbol level
LeonarddeR Nov 4, 2023
499b8d7
Validation fixes
LeonarddeR Nov 4, 2023
c4e3d5f
Add enum comments
LeonarddeR Nov 4, 2023
4597568
Lint
LeonarddeR Nov 4, 2023
0762fa5
Return annotation
LeonarddeR Nov 4, 2023
d1fde7b
Merge remote-tracking branch 'origin/master' into nvdaControllerClient
LeonarddeR Nov 6, 2023
fb78bb9
Remove unnecessary queueHandler imports
LeonarddeR Nov 6, 2023
fb581d8
Add enum docstrings
LeonarddeR Nov 6, 2023
86e6eeb
Update source/NVDAHelper.py
LeonarddeR Nov 6, 2023
f1e8d33
Safer approach
LeonarddeR Nov 6, 2023
9ef20ef
Update docs
LeonarddeR Nov 6, 2023
af3cec1
Fixup remote build
LeonarddeR Nov 7, 2023
991a1dd
Fixup changelog
LeonarddeR Nov 7, 2023
ae1f2e5
Lint
LeonarddeR Nov 7, 2023
5e5b187
Readme fix
LeonarddeR Nov 7, 2023
3f93d21
Update to the C# example
LeonarddeR Nov 7, 2023
9191f1b
update c example
LeonarddeR Nov 7, 2023
41e38c2
Apply suggestions from code review
LeonarddeR Nov 8, 2023
b0a0854
Review actions
LeonarddeR Nov 8, 2023
0cbbab3
Merge remote-tracking branch 'origin/master' into nvdaControllerClient
LeonarddeR Nov 23, 2023
ab3a08b
Expand docs about sync vs async
LeonarddeR Nov 23, 2023
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
46 changes: 46 additions & 0 deletions extras/controllerClient/examples/example_c.c
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
// Copyright(C) 2006-2023 NV Access Limited, Leonard de Ruijter
// This file is covered by the GNU Lesser General Public License, version 2.1.
// See the file license.txt for more details.

#define UNICODE
#include <windows.h>
#include <stdio.h>
#include "nvdaController.h"

error_status_t __stdcall onMarkReached(const wchar_t* name) {
wprintf(L"Reached SSML mark with name %s\n", name);
return ERROR_SUCCESS;
}

int main(int argc, char *argv[]) {
long res = nvdaController_testIfRunning();
if (res != 0) {
MessageBox(0, L"Error communicating with NVDA", L"Error", 0);
return 1;
}
for (int i = 0; i < 4; i++) {
nvdaController_speakText(L"This is a test speech message");
nvdaController_brailleMessage(L"This is a test braille message");
Sleep(1000);
}
wchar_t* ssml = (
L"<speak>"
L"This is one sentence. "
L"<mark name=\"test\" />"
L"<prosody pitch=\"200%\">This sentence is pronounced with higher pitch.</prosody>"
L"<mark name=\"test2\" />"
L"This is a third sentence. "
L"<mark name=\"test3\" />"
L"This is a fourth sentence. We will stay silent for a second after this one."
L"<break time=\"1000ms\" />"
L"<mark name=\"test4\" />"
L"This is a fifth sentence. "
L"<mark name=\"test5\" />"
L"</speak>"
);
nvdaController_setOnSsmlMarkReachedCallback(&onMarkReached);
nvdaController_speakSsml(ssml, SYMBOL_LEVEL_UNCHANGED, SPEECH_PRIORITY_NORMAL, FALSE);
nvdaController_setOnSsmlMarkReachedCallback(NULL);
nvdaController_brailleMessage(L"Test completed!");
return 0;
}
4 changes: 4 additions & 0 deletions extras/controllerClient/examples/example_csharp/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
.vs
bin
obj
*.sln
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>netstandard2.0</TargetFramework>
</PropertyGroup>

</Project>
142 changes: 142 additions & 0 deletions extras/controllerClient/examples/example_csharp/NVDA.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,142 @@
// Copyright(C) 2017-2023 NV Access Limited, Arnold Loubriat, Leonard de Ruijter
// This file is covered by the GNU Lesser General Public License, version 2.1.
// See the file license.txt for more details.

using System;
using System.ComponentModel;
using System.Runtime.InteropServices;

namespace NVAccess.NVDA
{
/// <summary>
/// C# wrapper class for the NVDA controller client DLL.
/// </summary>
static class NVDA
{
[DllImport("nvdaControllerClient.dll", CharSet = CharSet.Unicode)]
private static extern int nvdaController_brailleMessage(string message);

[DllImport("nvdaControllerClient.dll")]
private static extern int nvdaController_cancelSpeech();

[DllImport("nvdaControllerClient.dll", CharSet = CharSet.Unicode)]
private static extern int nvdaController_speakText(string text);

[DllImport("nvdaControllerClient.dll")]
private static extern int nvdaController_testIfRunning();

[DllImport("nvdaControllerClient.dll", CharSet = CharSet.Unicode)]
private static extern int nvdaController_getProcessId(out uint processId);

[DllImport("nvdaControllerClient.dll", CharSet = CharSet.Unicode)]
private static extern int nvdaController_speakSsml(
string ssml,
SymbolLevel symbolLevel = SymbolLevel.Unchanged,
SpeechPriority priority = SpeechPriority.Normal,
bool asynchronous = true
);

[UnmanagedFunctionPointer(CallingConvention.Winapi, CharSet = CharSet.Unicode)]
public delegate int OnSsmlMarkReached(string mark);

[DllImport("nvdaControllerClient.dll", CharSet = CharSet.Unicode)]
private static extern int nvdaController_setOnSsmlMarkReachedCallback(OnSsmlMarkReached callback);

/// <summary>
/// Indicates whether NVDA is running.
/// </summary>
public static bool IsRunning => nvdaController_testIfRunning() == 0;

/// <summary>
/// Tells NVDA to braille a message.
/// </summary>
/// <param name="message">The message to braille.</param>
public static void Braille(string message)
{
int res = nvdaController_brailleMessage(message);
if (res != 0)
{
throw new Win32Exception(res);
}

}

/// <summary>
/// Tells NVDA to immediately stop speaking.
/// </summary>
public static void CancelSpeech()
{
int res = nvdaController_cancelSpeech();
if (res != 0)
{
throw new Win32Exception(res);
}

}

/// <summary>
/// Tells NVDA to speak some text.
/// </summary>
/// <param name="text">The text to speak.</param>
/// <param name="interrupt">If true, NVDA will immediately speak the text, interrupting whatever it was speaking before.</param>
public static void Speak(string text, bool interrupt = true)
{
if (interrupt)
{
CancelSpeech();
}
int res = nvdaController_speakText(text);
if (res != 0)
{
throw new Win32Exception(res);
}
}

/// <summary>
/// Instructs NVDA to speak the given Speech Synthesis Markup Language (SSML).
/// </summary>
/// <param name="ssml">The ssml to speak.</param>
/// <param name="symbolLevel">The symbol verbosity level.</param>
/// <param name="priority">The priority of the speech sequence.</param>
/// <param name="asynchronous">Whether SSML should be spoken asynchronously.</param>
public static void SpeakSsml(
string ssml,
SymbolLevel symbolLevel = SymbolLevel.Unchanged,
SpeechPriority priority = SpeechPriority.Normal,
bool asynchronous = true,
OnSsmlMarkReached callback = null
)
{
int res = NVDA.nvdaController_setOnSsmlMarkReachedCallback(callback);
if (res != 0)
{
throw new Win32Exception(res);
}
res = nvdaController_speakSsml(ssml, symbolLevel, priority, asynchronous);
if (res != 0)
{
throw new Win32Exception(res);
}
res = NVDA.nvdaController_setOnSsmlMarkReachedCallback(null);
if (res != 0)
{
throw new Win32Exception(res);
}
}

/// <summary>
/// Retrieves the process identifier (PID) of NVDA's process.
/// </summary>
/// <returns></returns>
public static uint GetProcessId()
{
uint pid;
int res = nvdaController_getProcessId(out pid);
if (res != 0)
{
throw new Win32Exception(res);
}
return pid;
}
}
}
61 changes: 61 additions & 0 deletions extras/controllerClient/examples/example_csharp/Program.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
// Copyright(C) 2017-2023 NV Access Limited, Arnold Loubriat, Leonard de Ruijter
// This file is covered by the GNU Lesser General Public License, version 2.1.
// See the file license.txt for more details.

using System;
using System.Text;
using System.Threading.Tasks;

namespace NVAccess.NVDA
{
class Program
{
static async Task Main(string[] args)
{
// Test if NVDA is running.
if (!NVDA.IsRunning)
{
Console.Error.WriteLine("Error communicating with NVDA.");
}
else
{
Console.WriteLine($"NVDA is running as process {NVDA.GetProcessId()}");
// Speak and braille some messages.
for (int i = 0; i < 4; i++)
{
NVDA.Speak("This is a test client for NVDA!");
NVDA.Braille($"Time: {0.75 * i} seconds.");
await Task.Delay(625);
NVDA.CancelSpeech();
}

int onMarkReached(string name)
{
Console.WriteLine($"Reached SSML mark with name: {name}");
return 0;
}


StringBuilder ssmlBuilder = new StringBuilder();
ssmlBuilder
.Append("<speak>")
.Append("This is one sentence.")
.Append("<mark name=\"test\" />")
.Append("<prosody pitch=\"200%\">This sentence is pronounced with higher pitch.</prosody>")
.Append("<mark name=\"test2\" />")
.Append("This is a third sentence.")
.Append("<mark name=\"test3\" />")
.Append("This is a fourth sentence. We will stay silent for a second after this one.")
.Append("<break time=\"1000ms\" />")
.Append("<mark name=\"test4\" />")
.Append("This is a fifth sentence.")
.Append("<mark name=\"test5\" />")
.Append("</speak>");

NVDA.SpeakSsml(ssmlBuilder.ToString(), asynchronous: false, callback: onMarkReached);
NVDA.Braille("Test completed!");
}
Console.ReadKey(true);
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
// Copyright(C) 2017-2023 NV Access Limited, Arnold Loubriat, Leonard de Ruijter
// This file is covered by the GNU Lesser General Public License, version 2.1.
// See the file license.txt for more details.

namespace NVAccess.NVDA
{
/// <summary>
/// Facilitates the ability to prioritize speech.
/// This should match the SPEECH_PRIORITY enum in nvdaController.h, which itself matches the speech.priorities.SpeechPriority enum in NVDA.
/// </summary>
public enum SpeechPriority
{
/// <summary>
/// Indicates that a speech sequence should have normal priority.
/// </summary>
Normal = 0,
/// <summary>
/// Indicates that a speech sequence should be spoken after the next utterance of lower priority is complete.
/// </summary>
Next = 1,
/// <summary>
/// Indicates that a speech sequence is very important and should be spoken right now,
/// interrupting low priority speech.
/// After it is spoken, interrupted speech will resume.
/// Note that this does not interrupt previously queued speech at the same priority.
/// </summary>
Now = 2
}
}
20 changes: 20 additions & 0 deletions extras/controllerClient/examples/example_csharp/SymbolLevel.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
// Copyright(C) 2017-2023 NV Access Limited, Arnold Loubriat, Leonard de Ruijter
// This file is covered by the GNU Lesser General Public License, version 2.1.
// See the file license.txt for more details.

namespace NVAccess.NVDA
{
/// <summary>
/// The desired symbol level in a speech sequence.
/// This should match the SYMBOL_LEVEL enum in nvdaController.h, which itself matches the characterProcessing.SymbolLevel enum in NVDA.
/// </summary>
public enum SymbolLevel
{
None = 0,
Some = 100,
Most = 200,
All = 300,
Char = 1000,
Unchanged = -1
}
}
51 changes: 51 additions & 0 deletions extras/controllerClient/examples/example_python.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
# A part of NonVisual Desktop Access (NVDA)
# Copyright (C) 2006-2023 NV Access Limited, Łukasz Golonka, Leonard de Ruijter
# This file is covered by the GNU Lesser General Public License, version 2.1.
# See the file license.txt for more details.

import ctypes
import time

# Load the NVDA client library
clientLib = ctypes.windll.LoadLibrary("./nvdaControllerClient.dll")

# Test if NVDA is running, and if its not show a message
res = clientLib.nvdaController_testIfRunning()
if res != 0:
errorMessage = str(ctypes.WinError(res))
ctypes.windll.user32.MessageBoxW(0, f"Error: {errorMessage}", "Error communicating with NVDA", 0)

# Speak and braille some messages
for count in range(4):
clientLib.nvdaController_speakText("This is a test client for NVDA")
clientLib.nvdaController_brailleMessage("Time: %g seconds" % (0.75 * count))
time.sleep(0.625)
clientLib.nvdaController_cancelSpeech()


# Test SSML output
@ctypes.WINFUNCTYPE(ctypes.c_ulong, ctypes.c_wchar_p)
def onMarkReached(name: str) -> int:
print(f"Reached SSML mark with name: {name}")
return 0


ssml = (
'<speak>'
'This is one sentence. '
'<mark name="test" />'
'<prosody pitch="200%">This sentence is pronounced with higher pitch.</prosody>'
'<mark name="test2" />'
'This is a third sentence. '
'<mark name="test3" />'
'This is a fourth sentence. We will stay silent for a second after this one.'
'<break time="1000ms" />'
'<mark name="test4" />'
'This is a fifth sentence. '
'<mark name="test5" />'
'</speak>'
)
clientLib.nvdaController_setOnSsmlMarkReachedCallback(onMarkReached)
clientLib.nvdaController_speakSsml(ssml, -1, 0, False)
clientLib.nvdaController_setOnSsmlMarkReachedCallback(None)
clientLib.nvdaController_brailleMessage("Test completed!")
Loading