make speech2 a dll + start binding it

This commit is contained in:
Lily Tsuru 2024-07-18 03:54:12 -04:00
parent 20f59c5663
commit bd048875c8
11 changed files with 230 additions and 299 deletions

View file

@ -5,3 +5,7 @@ build:
clean: clean:
rm -rf SAPIServer/bin SAPIServer/obj rm -rf SAPIServer/bin SAPIServer/obj
make -C speech2 clean make -C speech2 clean
format:
cd SAPIServer; for f in *.cs; do clang-format -i $f; done; cd ..;

View file

@ -3,72 +3,67 @@ using System.Collections.Generic;
using System.Net; using System.Net;
using System.Text; using System.Text;
using System.Threading; using System.Threading;
using System.Threading.Tasks;
namespace SAPIServer namespace SAPIServer {
{
/// <summary> /// <summary>
/// Simple routing HTTP server for .NET Framework /// Simple routing HTTP server for .NET Framework
/// </summary> /// </summary>
class HTTPServer class HTTPServer {
{
private bool listening = false; private bool listening = false;
private HttpListener listener; private HttpListener listener;
private Dictionary<string, Func<HttpListenerContext, byte[]>> GETListeners; private Dictionary<string, Func<HttpListenerContext, byte[]>> GETListeners;
private Dictionary<string, Func<HttpListenerContext, byte[]>> POSTListeners; private Dictionary<string, Func<HttpListenerContext, byte[]>> POSTListeners;
public HTTPServer() public HTTPServer() {
{
listener = new HttpListener(); listener = new HttpListener();
this.GETListeners = new Dictionary<string, Func<HttpListenerContext, byte[]>>(); this.GETListeners = new Dictionary<string, Func<HttpListenerContext, byte[]>>();
this.POSTListeners = new Dictionary<string, Func<HttpListenerContext, byte[]>>(); this.POSTListeners = new Dictionary<string, Func<HttpListenerContext, byte[]>>();
} }
public void Get(string uri, Func<HttpListenerContext, byte[]> handler) public void Get(string uri, Func<HttpListenerContext, byte[]> handler) {
{ if(GETListeners.ContainsKey(uri))
if (GETListeners.ContainsKey(uri)) throw new InvalidOperationException("A handler by that URI already exists."); throw new InvalidOperationException("A handler by that URI already exists.");
if (!uri.StartsWith("/")) throw new ArgumentException("URIs must start with /"); if(!uri.StartsWith("/"))
throw new ArgumentException("URIs must start with /");
this.GETListeners.Add(uri, handler); this.GETListeners.Add(uri, handler);
} }
public void Post(string uri, Func<HttpListenerContext, byte[]> handler) public void Post(string uri, Func<HttpListenerContext, byte[]> handler) {
{ if(POSTListeners.ContainsKey(uri))
if (POSTListeners.ContainsKey(uri)) throw new InvalidOperationException("A handler by that URI already exists."); throw new InvalidOperationException("A handler by that URI already exists.");
if (!uri.StartsWith("/")) throw new ArgumentException("URIs must start with /"); if(!uri.StartsWith("/"))
throw new ArgumentException("URIs must start with /");
this.POSTListeners.Add(uri, handler); this.POSTListeners.Add(uri, handler);
} }
public void Listen(ushort port) public void Listen(ushort port) {
{ if(listening)
if (listening) throw new InvalidOperationException("This HTTPServer is already listening."); throw new InvalidOperationException("This HTTPServer is already listening.");
listening = true; listening = true;
listener.Prefixes.Add(string.Format("http://*:{0}/", port)); listener.Prefixes.Add(string.Format("http://*:{0}/", port));
listener.Start(); listener.Start();
while (listener.IsListening)
{ while(listener.IsListening) {
var context = listener.GetContext(); var context = listener.GetContext();
ThreadPool.QueueUserWorkItem(_ => ThreadPool.QueueUserWorkItem(
{ _ => {
var uri = context.Request.RawUrl.Split('?')[0]; var uri = context.Request.RawUrl.Split('?')[0];
var ip = context.Request.RemoteEndPoint.Address; var ip = context.Request.RemoteEndPoint.Address;
try try {
{
Dictionary<string, Func<HttpListenerContext, byte[]>> handlerDict; Dictionary<string, Func<HttpListenerContext, byte[]>> handlerDict;
// TODO: Make query params parsable // TODO: Make query params parsable
byte[] response; byte[] response;
switch (context.Request.HttpMethod) switch(context.Request.HttpMethod) {
{ case "GET": {
case "GET":
{
handlerDict = GETListeners; handlerDict = GETListeners;
break; break;
} }
case "POST": case "POST": {
{
handlerDict = POSTListeners; handlerDict = POSTListeners;
break; break;
} }
default: default: {
{
response = Encoding.UTF8.GetBytes($"Method not allowed: {context.Request.HttpMethod}"); response = Encoding.UTF8.GetBytes($"Method not allowed: {context.Request.HttpMethod}");
context.Response.StatusCode = 405; context.Response.StatusCode = 405;
context.Response.ContentType = "text/plain"; context.Response.ContentType = "text/plain";
@ -78,35 +73,31 @@ namespace SAPIServer
return; return;
} }
} }
if (!handlerDict.TryGetValue(uri, out var handler)) if(!handlerDict.TryGetValue(uri, out var handler)) {
{
response = Encoding.UTF8.GetBytes($"No route defined for {uri}"); response = Encoding.UTF8.GetBytes($"No route defined for {uri}");
context.Response.StatusCode = 404; context.Response.StatusCode = 404;
context.Response.ContentType = "text/plain"; context.Response.ContentType = "text/plain";
} } else {
else try {
{
try
{
response = handler(context); response = handler(context);
} catch (Exception e) } catch(Exception e) {
{
response = Encoding.UTF8.GetBytes("Internal Server Error"); response = Encoding.UTF8.GetBytes("Internal Server Error");
context.Response.StatusCode = 500; context.Response.StatusCode = 500;
context.Response.ContentType = "text/plain"; context.Response.ContentType = "text/plain";
Console.Error.WriteLine($"[{DateTime.Now:u}] {ip} - {context.Request.HttpMethod} {context.Request.RawUrl} - Handler Failed: {e.Message}"); Console.Error.WriteLine(
$"[{DateTime.Now:u}] {ip} - {context.Request.HttpMethod} {context.Request.RawUrl} - Handler Failed: {e.Message}");
} }
} }
context.Response.ContentLength64 = response.Length; context.Response.ContentLength64 = response.Length;
context.Response.OutputStream.Write(response, 0, response.Length); context.Response.OutputStream.Write(response, 0, response.Length);
context.Response.OutputStream.Close(); context.Response.OutputStream.Close();
Console.WriteLine($"[{DateTime.Now:u}] {ip} - {context.Request.HttpMethod} {context.Request.RawUrl} - {context.Response.StatusCode}"); Console.WriteLine(
} catch (Exception e) $"[{DateTime.Now:u}] {ip} - {context.Request.HttpMethod} {context.Request.RawUrl} - {context.Response.StatusCode}");
{ } catch(Exception e) {
Console.Error.WriteLine($"[{DateTime.Now:u}] {ip} - {context.Request.HttpMethod} {context.Request.RawUrl} - Exception: {e.Message}"); Console.Error.WriteLine(
$"[{DateTime.Now:u}] {ip} - {context.Request.HttpMethod} {context.Request.RawUrl} - Exception: {e.Message}");
} }
}); });
} }
} }

View file

@ -6,57 +6,40 @@ using System.Speech.Synthesis;
using System.Text; using System.Text;
using Newtonsoft.Json; using Newtonsoft.Json;
namespace SAPIServer namespace SAPIServer {
{ class Program {
class Program static void Main(string[] args) {
{ if(args.Length < 1 || !ushort.TryParse(args[0], out var port)) {
static void Main(string[] args)
{
if (args.Length < 1 || !ushort.TryParse(args[0], out var port))
{
Console.Error.WriteLine("Usage: SAPIServer.exe <port>"); Console.Error.WriteLine("Usage: SAPIServer.exe <port>");
Environment.Exit(1); Environment.Exit(1);
return; return;
} }
var http = new HTTPServer(); var http = new HTTPServer();
http.Get("/api/voices", ctx => http.Get("/api/voices", ctx => {
{ using(var synth = new SpeechSynthesizer()) {
using (var synth = new SpeechSynthesizer())
{
ctx.Response.ContentType = "application/json"; ctx.Response.ContentType = "application/json";
return Encoding.UTF8.GetBytes(JsonConvert.SerializeObject(new VoicesResponse return Encoding.UTF8.GetBytes(
{ JsonConvert.SerializeObject(new VoicesResponse { voices = synth.GetInstalledVoices().Select(v => v.VoiceInfo.Name).ToArray() }));
voices = synth.GetInstalledVoices().Select(v => v.VoiceInfo.Name).ToArray()
}));
} }
}); });
http.Post("/api/synthesize", ctx => http.Post("/api/synthesize", ctx => {
{
SynthesizePayload body; SynthesizePayload body;
try try {
{
string bodyraw; string bodyraw;
using (var reader = new StreamReader(ctx.Request.InputStream)) using(var reader = new StreamReader(ctx.Request.InputStream)) {
{
bodyraw = reader.ReadToEnd(); bodyraw = reader.ReadToEnd();
} }
body = JsonConvert.DeserializeObject<SynthesizePayload>(bodyraw); body = JsonConvert.DeserializeObject<SynthesizePayload>(bodyraw);
} } catch(Exception) {
catch (Exception e)
{
ctx.Response.StatusCode = 400; ctx.Response.StatusCode = 400;
return Encoding.UTF8.GetBytes("Bad payload"); return Encoding.UTF8.GetBytes("Bad payload");
} }
if (body == null || body.text == null || body.voice == null) if(body == null || body.text == null || body.voice == null) {
{
ctx.Response.StatusCode = 400; ctx.Response.StatusCode = 400;
return Encoding.UTF8.GetBytes("Bad payload"); return Encoding.UTF8.GetBytes("Bad payload");
} }
using (var ms = new MemoryStream()) using(var ms = new MemoryStream()) using(var synth = new SpeechSynthesizer()) {
using (var synth = new SpeechSynthesizer()) if(!synth.GetInstalledVoices().Any(v => v.VoiceInfo.Name == body.voice)) {
{
if (!synth.GetInstalledVoices().Any(v => v.VoiceInfo.Name == body.voice))
{
ctx.Response.StatusCode = 400; ctx.Response.StatusCode = 400;
return Encoding.UTF8.GetBytes("Voice not found"); return Encoding.UTF8.GetBytes("Voice not found");
} }
@ -67,7 +50,7 @@ namespace SAPIServer
return ms.ToArray(); return ms.ToArray();
} }
}); });
Console.WriteLine($"[{ DateTime.Now:u}] Starting HTTP server on port {port}"); Console.WriteLine($"[{DateTime.Now:u}] Starting HTTP server on port {port}");
http.Listen(port); http.Listen(port);
} }
} }

View file

@ -10,6 +10,7 @@
<ItemGroup> <ItemGroup>
<PackageReference Include="Newtonsoft.Json" Version="13.0.3" /> <PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
<PackageReference Include="Microsoft.Bcl.Async" Version="1.0.168" />
<Reference Include="System" /> <Reference Include="System" />
<Reference Include="System.Core" /> <Reference Include="System.Core" />
<Reference Include="System.Speech" /> <Reference Include="System.Speech" />

37
SAPIServer/SpeechDLL.cs Normal file
View file

@ -0,0 +1,37 @@
using System;
using System.Runtime.InteropServices;
namespace SAPIServer {
// Sync with C++ code.
public enum EngineType : int { ET_SAPI4, ET_SAPI5, ET_DECTALK }
// Speech2 DLL API.
internal class SpeechDLL {
[DllImport("speech2")]
public static extern IntPtr speech2_create_api(EngineType type);
[DllImport("speech2")]
public static extern void speech2_destroy_api(IntPtr pAPI);
}
// Native binding of speech2 library from C++ to C#.
public class SpeechAPI : IDisposable {
private IntPtr handle = IntPtr.Zero;
public SpeechAPI(EngineType type) {
handle = SpeechDLL.speech2_create_api(type);
if(handle == IntPtr.Zero)
throw new InvalidOperationException("Failed to create speech API");
}
void IDisposable.Dispose() {
if(handle != IntPtr.Zero)
SpeechDLL.speech2_destroy_api(handle);
}
}
}

View file

@ -1,7 +1,5 @@
namespace SAPIServer namespace SAPIServer {
{ class SynthesizePayload {
class SynthesizePayload
{
public string voice { get; set; } public string voice { get; set; }
public string text { get; set; } public string text { get; set; }
} }

View file

@ -1,7 +1,5 @@
namespace SAPIServer namespace SAPIServer {
{ class VoicesResponse {
class VoicesResponse
{
public string[] voices { get; set; } public string[] voices { get; set; }
} }
} }

View file

@ -13,7 +13,7 @@ OBJS = $(addprefix $(OBJDIR)/,$(notdir $(CXXSRCS:.cpp=.o)))
.PHONY: all dumpinfo clean matrix .PHONY: all dumpinfo clean matrix
all: $(BINDIR)/$(NAME).exe all: $(BINDIR)/$(NAME).dll
# dir rules # dir rules
$(BINDIR)/: $(BINDIR)/:

View file

@ -1,7 +1,7 @@
# Base compiler flags. Only change if you *explicitly* know what you're doing. # Base compiler flags. Only change if you *explicitly* know what you're doing.
BASE_CCFLAGS = -MMD -std=gnu17 -fpermissive -fno-pic -fno-pie -msse -Iinclude -Isrc -D_UCRT -D_WIN32_WINNT=0x0501 BASE_CCFLAGS = -MMD -fvisibility=hidden -std=gnu17 -fpermissive -fno-ident -msse -Iinclude -Isrc -D_UCRT -D_WIN32_WINNT=0x0501
BASE_CXXFLAGS = -MMD -std=c++20 -fpermissive -fno-pic -fno-pie -fno-rtti -msse -Iinclude -Isrc -Ithird_party -D_UCRT -D_WIN32_WINNT=0x0501 BASE_CXXFLAGS = -MMD -fvisibility=hidden -std=c++20 -fpermissive -fno-ident -msse -Iinclude -Isrc -Ithird_party -D_UCRT -D_WIN32_WINNT=0x0501
BASE_LDFLAGS = -mwindows -static -static-libgcc -lkernel32 -lshell32 -luser32 -luuid -lole32 BASE_LDFLAGS = -Wl,--subsystem=windows -fvisibility=hidden -shared -lkernel32 -lshell32 -luser32 -luuid -lole32
Release_Valid = yes Release_Valid = yes
Release_CCFLAGS = -O3 -ffast-math -fomit-frame-pointer -DNDEBUG Release_CCFLAGS = -O3 -ffast-math -fomit-frame-pointer -DNDEBUG

View file

@ -1,5 +1,4 @@
# TODO: Link DLL $(BINDIR)/$(NAME).dll: $(BINDIR)/ $(OBJDIR)/ $(OBJS)
$(BINDIR)/$(NAME).exe: $(BINDIR)/ $(OBJDIR)/ $(OBJS)
echo -e "\e[92mLinking binary $@\e[0m" echo -e "\e[92mLinking binary $@\e[0m"
$(CXX) $(OBJS) $(BASE_LDFLAGS) $($(CONFIG)_LDFLAGS) -o $@ $(CXX) $(OBJS) $(BASE_LDFLAGS) $($(CONFIG)_LDFLAGS) -o $@

View file

@ -1,120 +1,40 @@
#include <stdio.h> #include <windows.h>
#include <winscard.h>
#include <base/BThread.hpp>
#include <base/Mutex.hpp>
#include <base/Thread.hpp>
#include "speechapi.hpp" #include "speechapi.hpp"
// args #define SP2_EXPORT __declspec(dllexport)
// -v <voice> (voice to use)
// -s 4|5 (what SAPI version to use)
// -p 0..100 (pitch)
// -s 0..100
// [message]
int main(int argc, char** argv) { enum class EngineType : int { ET_SAPI4, ET_SAPI5, ET_DECTALK };
#if 1
if(FAILED(CoInitialize(nullptr))) { extern "C" {
printf("Couldn't initalize COM\n");
return 1; SP2_EXPORT void* speech2_create_api(EngineType type) {
switch(type) {
case EngineType::ET_SAPI4: return static_cast<void*>(ISpeechAPI::CreateSapi4());
default: return nullptr;
} }
}
auto api = ISpeechAPI::CreateSapi4();
if(!api) { SP2_EXPORT void speech2_destroy_api(void* engine) {
printf("Couldn't allocate memory for speech API\n"); if(engine)
return 1; delete static_cast<ISpeechAPI*>(engine);
} }
if(auto hRes = api->Initialize(); FAILED(hRes)) { }
printf("Couldn't initalize SAPI 4 (hr: %08x)\n", hRes);
return 1; BOOL WINAPI DllMain(HINSTANCE hinstDLL, DWORD fdwReason, LPVOID lpvReserved) {
} switch(fdwReason) {
case DLL_PROCESS_ATTACH: break;
#if 1
auto voices = api->GetVoices(); case DLL_THREAD_ATTACH: break;
printf("Available voices:\n");
for(auto& voice : voices) { case DLL_THREAD_DETACH: break;
auto& guid = voice.guid;
case DLL_PROCESS_DETACH:
printf("%s (GUID {%08X-%04X-%04X-%02X%02X-%02X%02X%02X%02X%02X%02X})\n", voice.voiceName.c_str(), guid.Data1, guid.Data2, guid.Data3, if(lpvReserved != nullptr) {
guid.Data4[0], guid.Data4[1], guid.Data4[2], guid.Data4[3], guid.Data4[4], guid.Data4[5], guid.Data4[6], guid.Data4[7]); break; // do not do cleanup if process termination scenario
} }
#endif break;
}
if(auto hRes = api->SelectVoice("Sam"); FAILED(hRes)) { return TRUE;
printf("Test: Couldn't select Microsoft Sam\n");
return 1;
} else {
printf("Test: Selected Microsoft Sam\n");
}
#if 1
if(auto hRes = api->SelectVoice("Mike"); FAILED(hRes)) {
printf("Test: Couldn't select Microsoft Mike\n");
return 1;
} else {
printf("Test: Seleced Microsoft Mike\n");
}
#endif
printf("Test: Selected voices successfully\n");
printf("Test: Destroying voice\n");
delete api;
CoUninitialize();
#endif
// condvar/threading tests
#if 0
static auto cv = base::osdep::BCondVar_Create();
static auto PrintMutex = base::Mutex{};
static auto n = 0;
//auto n = 0;
Sleep(100);
base::Thread t([]() {
base::osdep::BCondVar_Wait(cv, [](void* ctx) {
base::LockGuard lk(PrintMutex);
printf("t: wait predicate called %d\n", *static_cast<int*>(ctx));
return *static_cast<int*>(ctx) == 4;
}, static_cast<void*>(&n));
{
base::LockGuard lk(PrintMutex);
printf("t: condvar exited wait!\n");
}
});
base::Thread t2([]() {
base::osdep::BCondVar_Wait(cv, [](void* ctx) {
base::LockGuard lk(PrintMutex);
printf("t2: wait predicate called %d\n", *static_cast<int*>(ctx));
return *static_cast<int*>(ctx) == 4;
}, static_cast<void*>(&n));
{
base::LockGuard lk(PrintMutex);
printf("t2: condvar exited wait!\n");
}
});
for(auto i = 0; i < 4; ++i) {
base::osdep::BCondVar_SignalOne(cv);
n++;
Sleep(100);
}
t2.Join();
t.Join();
base::osdep::BCondVar_Destroy(cv);
#endif
return 0;
} }