diff --git a/Justfile b/Justfile
index 81de9d8..16831e8 100644
--- a/Justfile
+++ b/Justfile
@@ -5,3 +5,7 @@ build:
clean:
rm -rf SAPIServer/bin SAPIServer/obj
make -C speech2 clean
+
+
+format:
+ cd SAPIServer; for f in *.cs; do clang-format -i $f; done; cd ..;
diff --git a/SAPIServer/HTTPServer.cs b/SAPIServer/HTTPServer.cs
index 654856a..bbad5f2 100644
--- a/SAPIServer/HTTPServer.cs
+++ b/SAPIServer/HTTPServer.cs
@@ -3,112 +3,103 @@ using System.Collections.Generic;
using System.Net;
using System.Text;
using System.Threading;
+using System.Threading.Tasks;
-namespace SAPIServer
-{
- ///
- /// Simple routing HTTP server for .NET Framework
- ///
- class HTTPServer
- {
- private bool listening = false;
- private HttpListener listener;
- private Dictionary> GETListeners;
- private Dictionary> POSTListeners;
+namespace SAPIServer {
+ ///
+ /// Simple routing HTTP server for .NET Framework
+ ///
+ class HTTPServer {
+ private bool listening = false;
+ private HttpListener listener;
+ private Dictionary> GETListeners;
+ private Dictionary> POSTListeners;
- public HTTPServer()
- {
- listener = new HttpListener();
- this.GETListeners = new Dictionary>();
- this.POSTListeners = new Dictionary>();
- }
+ public HTTPServer() {
+ listener = new HttpListener();
+ this.GETListeners = new Dictionary>();
+ this.POSTListeners = new Dictionary>();
+ }
- public void Get(string uri, Func handler)
- {
- if (GETListeners.ContainsKey(uri)) throw new InvalidOperationException("A handler by that URI already exists.");
- if (!uri.StartsWith("/")) throw new ArgumentException("URIs must start with /");
- this.GETListeners.Add(uri, handler);
- }
+ public void Get(string uri, Func handler) {
+ if(GETListeners.ContainsKey(uri))
+ throw new InvalidOperationException("A handler by that URI already exists.");
+ if(!uri.StartsWith("/"))
+ throw new ArgumentException("URIs must start with /");
+ this.GETListeners.Add(uri, handler);
+ }
- public void Post(string uri, Func handler)
- {
- if (POSTListeners.ContainsKey(uri)) throw new InvalidOperationException("A handler by that URI already exists.");
- if (!uri.StartsWith("/")) throw new ArgumentException("URIs must start with /");
- this.POSTListeners.Add(uri, handler);
- }
+ public void Post(string uri, Func handler) {
+ if(POSTListeners.ContainsKey(uri))
+ throw new InvalidOperationException("A handler by that URI already exists.");
+ if(!uri.StartsWith("/"))
+ throw new ArgumentException("URIs must start with /");
+ this.POSTListeners.Add(uri, handler);
+ }
- public void Listen(ushort port)
- {
- if (listening) throw new InvalidOperationException("This HTTPServer is already listening.");
- listening = true;
- listener.Prefixes.Add(string.Format("http://*:{0}/", port));
- listener.Start();
- while (listener.IsListening)
- {
- var context = listener.GetContext();
- ThreadPool.QueueUserWorkItem(_ =>
- {
- var uri = context.Request.RawUrl.Split('?')[0];
- var ip = context.Request.RemoteEndPoint.Address;
- try
- {
- Dictionary> handlerDict;
- // TODO: Make query params parsable
- byte[] response;
- switch (context.Request.HttpMethod)
- {
- case "GET":
- {
- handlerDict = GETListeners;
- break;
- }
- case "POST":
- {
- handlerDict = POSTListeners;
- break;
- }
- default:
- {
- response = Encoding.UTF8.GetBytes($"Method not allowed: {context.Request.HttpMethod}");
- context.Response.StatusCode = 405;
- context.Response.ContentType = "text/plain";
- context.Response.ContentLength64 = response.Length;
- context.Response.OutputStream.Write(response, 0, response.Length);
- context.Response.OutputStream.Close();
- return;
- }
- }
- if (!handlerDict.TryGetValue(uri, out var handler))
- {
- response = Encoding.UTF8.GetBytes($"No route defined for {uri}");
- context.Response.StatusCode = 404;
- context.Response.ContentType = "text/plain";
- }
- else
- {
- try
- {
- response = handler(context);
- } catch (Exception e)
- {
- response = Encoding.UTF8.GetBytes("Internal Server Error");
- context.Response.StatusCode = 500;
- context.Response.ContentType = "text/plain";
- Console.Error.WriteLine($"[{DateTime.Now:u}] {ip} - {context.Request.HttpMethod} {context.Request.RawUrl} - Handler Failed: {e.Message}");
- }
- }
+ public void Listen(ushort port) {
+ if(listening)
+ throw new InvalidOperationException("This HTTPServer is already listening.");
+ listening = true;
+ listener.Prefixes.Add(string.Format("http://*:{0}/", port));
+ listener.Start();
- context.Response.ContentLength64 = response.Length;
- context.Response.OutputStream.Write(response, 0, response.Length);
- context.Response.OutputStream.Close();
- Console.WriteLine($"[{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}");
- }
+ while(listener.IsListening) {
+ var context = listener.GetContext();
+ ThreadPool.QueueUserWorkItem(
+ _ => {
+ var uri = context.Request.RawUrl.Split('?')[0];
+ var ip = context.Request.RemoteEndPoint.Address;
+ try {
+ Dictionary> handlerDict;
+ // TODO: Make query params parsable
+ byte[] response;
+ switch(context.Request.HttpMethod) {
+ case "GET": {
+ handlerDict = GETListeners;
+ break;
+ }
+ case "POST": {
+ handlerDict = POSTListeners;
+ break;
+ }
+ default: {
+ response = Encoding.UTF8.GetBytes($"Method not allowed: {context.Request.HttpMethod}");
+ context.Response.StatusCode = 405;
+ context.Response.ContentType = "text/plain";
+ context.Response.ContentLength64 = response.Length;
+ context.Response.OutputStream.Write(response, 0, response.Length);
+ context.Response.OutputStream.Close();
+ return;
+ }
+ }
+ if(!handlerDict.TryGetValue(uri, out var handler)) {
+ response = Encoding.UTF8.GetBytes($"No route defined for {uri}");
+ context.Response.StatusCode = 404;
+ context.Response.ContentType = "text/plain";
+ } else {
+ try {
+ response = handler(context);
+ } catch(Exception e) {
+ response = Encoding.UTF8.GetBytes("Internal Server Error");
+ context.Response.StatusCode = 500;
+ context.Response.ContentType = "text/plain";
+ Console.Error.WriteLine(
+ $"[{DateTime.Now:u}] {ip} - {context.Request.HttpMethod} {context.Request.RawUrl} - Handler Failed: {e.Message}");
+ }
+ }
- });
- }
- }
- }
+ context.Response.ContentLength64 = response.Length;
+ context.Response.OutputStream.Write(response, 0, response.Length);
+ context.Response.OutputStream.Close();
+ Console.WriteLine(
+ $"[{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}");
+ }
+ });
+ }
+ }
+ }
}
diff --git a/SAPIServer/Program.cs b/SAPIServer/Program.cs
index 3476713..06c2a97 100644
--- a/SAPIServer/Program.cs
+++ b/SAPIServer/Program.cs
@@ -6,69 +6,52 @@ using System.Speech.Synthesis;
using System.Text;
using Newtonsoft.Json;
-namespace SAPIServer
-{
- class Program
- {
- static void Main(string[] args)
- {
- if (args.Length < 1 || !ushort.TryParse(args[0], out var port))
- {
- Console.Error.WriteLine("Usage: SAPIServer.exe ");
- Environment.Exit(1);
- return;
- }
- var http = new HTTPServer();
- http.Get("/api/voices", ctx =>
- {
- using (var synth = new SpeechSynthesizer())
- {
- ctx.Response.ContentType = "application/json";
- return Encoding.UTF8.GetBytes(JsonConvert.SerializeObject(new VoicesResponse
- {
- voices = synth.GetInstalledVoices().Select(v => v.VoiceInfo.Name).ToArray()
- }));
- }
- });
- http.Post("/api/synthesize", ctx =>
- {
- SynthesizePayload body;
- try
- {
- string bodyraw;
- using (var reader = new StreamReader(ctx.Request.InputStream))
- {
- bodyraw = reader.ReadToEnd();
- }
- body = JsonConvert.DeserializeObject(bodyraw);
- }
- catch (Exception e)
- {
- ctx.Response.StatusCode = 400;
- return Encoding.UTF8.GetBytes("Bad payload");
- }
- if (body == null || body.text == null || body.voice == null)
- {
- ctx.Response.StatusCode = 400;
- return Encoding.UTF8.GetBytes("Bad payload");
- }
- using (var ms = new MemoryStream())
- using (var synth = new SpeechSynthesizer())
- {
- if (!synth.GetInstalledVoices().Any(v => v.VoiceInfo.Name == body.voice))
- {
- ctx.Response.StatusCode = 400;
- return Encoding.UTF8.GetBytes("Voice not found");
- }
- synth.SelectVoice(body.voice);
- synth.SetOutputToWaveStream(ms);
- synth.Speak(body.text);
- ctx.Response.ContentType = "audio/wav";
- return ms.ToArray();
- }
- });
- Console.WriteLine($"[{ DateTime.Now:u}] Starting HTTP server on port {port}");
- http.Listen(port);
- }
- }
+namespace SAPIServer {
+ class Program {
+ static void Main(string[] args) {
+ if(args.Length < 1 || !ushort.TryParse(args[0], out var port)) {
+ Console.Error.WriteLine("Usage: SAPIServer.exe ");
+ Environment.Exit(1);
+ return;
+ }
+ var http = new HTTPServer();
+ http.Get("/api/voices", ctx => {
+ using(var synth = new SpeechSynthesizer()) {
+ ctx.Response.ContentType = "application/json";
+ return Encoding.UTF8.GetBytes(
+ JsonConvert.SerializeObject(new VoicesResponse { voices = synth.GetInstalledVoices().Select(v => v.VoiceInfo.Name).ToArray() }));
+ }
+ });
+ http.Post("/api/synthesize", ctx => {
+ SynthesizePayload body;
+ try {
+ string bodyraw;
+ using(var reader = new StreamReader(ctx.Request.InputStream)) {
+ bodyraw = reader.ReadToEnd();
+ }
+ body = JsonConvert.DeserializeObject(bodyraw);
+ } catch(Exception) {
+ ctx.Response.StatusCode = 400;
+ return Encoding.UTF8.GetBytes("Bad payload");
+ }
+ if(body == null || body.text == null || body.voice == null) {
+ ctx.Response.StatusCode = 400;
+ return Encoding.UTF8.GetBytes("Bad payload");
+ }
+ using(var ms = new MemoryStream()) using(var synth = new SpeechSynthesizer()) {
+ if(!synth.GetInstalledVoices().Any(v => v.VoiceInfo.Name == body.voice)) {
+ ctx.Response.StatusCode = 400;
+ return Encoding.UTF8.GetBytes("Voice not found");
+ }
+ synth.SelectVoice(body.voice);
+ synth.SetOutputToWaveStream(ms);
+ synth.Speak(body.text);
+ ctx.Response.ContentType = "audio/wav";
+ return ms.ToArray();
+ }
+ });
+ Console.WriteLine($"[{DateTime.Now:u}] Starting HTTP server on port {port}");
+ http.Listen(port);
+ }
+ }
}
diff --git a/SAPIServer/SAPIServer.csproj b/SAPIServer/SAPIServer.csproj
index c3c4251..5a75f0b 100644
--- a/SAPIServer/SAPIServer.csproj
+++ b/SAPIServer/SAPIServer.csproj
@@ -10,6 +10,7 @@
+
diff --git a/SAPIServer/SpeechDLL.cs b/SAPIServer/SpeechDLL.cs
new file mode 100644
index 0000000..f306a93
--- /dev/null
+++ b/SAPIServer/SpeechDLL.cs
@@ -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);
+ }
+ }
+
+}
diff --git a/SAPIServer/SynthesizePayload.cs b/SAPIServer/SynthesizePayload.cs
index 333565d..0e3eddb 100644
--- a/SAPIServer/SynthesizePayload.cs
+++ b/SAPIServer/SynthesizePayload.cs
@@ -1,8 +1,6 @@
-namespace SAPIServer
-{
- class SynthesizePayload
- {
- public string voice { get; set; }
- public string text { get; set; }
- }
+namespace SAPIServer {
+ class SynthesizePayload {
+ public string voice { get; set; }
+ public string text { get; set; }
+ }
}
diff --git a/SAPIServer/VoicesResponse.cs b/SAPIServer/VoicesResponse.cs
index 214d8e7..d903621 100644
--- a/SAPIServer/VoicesResponse.cs
+++ b/SAPIServer/VoicesResponse.cs
@@ -1,7 +1,5 @@
-namespace SAPIServer
-{
- class VoicesResponse
- {
- public string[] voices { get; set; }
- }
+namespace SAPIServer {
+ class VoicesResponse {
+ public string[] voices { get; set; }
+ }
}
diff --git a/speech2/Makefile b/speech2/Makefile
index 0886749..81eefa7 100644
--- a/speech2/Makefile
+++ b/speech2/Makefile
@@ -13,7 +13,7 @@ OBJS = $(addprefix $(OBJDIR)/,$(notdir $(CXXSRCS:.cpp=.o)))
.PHONY: all dumpinfo clean matrix
-all: $(BINDIR)/$(NAME).exe
+all: $(BINDIR)/$(NAME).dll
# dir rules
$(BINDIR)/:
diff --git a/speech2/build/configs.mk b/speech2/build/configs.mk
index 793dfbb..bcc3dbe 100644
--- a/speech2/build/configs.mk
+++ b/speech2/build/configs.mk
@@ -1,7 +1,7 @@
# 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_CXXFLAGS = -MMD -std=c++20 -fpermissive -fno-pic -fno-pie -fno-rtti -msse -Iinclude -Isrc -Ithird_party -D_UCRT -D_WIN32_WINNT=0x0501
-BASE_LDFLAGS = -mwindows -static -static-libgcc -lkernel32 -lshell32 -luser32 -luuid -lole32
+BASE_CCFLAGS = -MMD -fvisibility=hidden -std=gnu17 -fpermissive -fno-ident -msse -Iinclude -Isrc -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 = -Wl,--subsystem=windows -fvisibility=hidden -shared -lkernel32 -lshell32 -luser32 -luuid -lole32
Release_Valid = yes
Release_CCFLAGS = -O3 -ffast-math -fomit-frame-pointer -DNDEBUG
diff --git a/speech2/build/rules.mk b/speech2/build/rules.mk
index 2f3677b..38ec891 100644
--- a/speech2/build/rules.mk
+++ b/speech2/build/rules.mk
@@ -1,5 +1,4 @@
-# TODO: Link DLL
-$(BINDIR)/$(NAME).exe: $(BINDIR)/ $(OBJDIR)/ $(OBJS)
+$(BINDIR)/$(NAME).dll: $(BINDIR)/ $(OBJDIR)/ $(OBJS)
echo -e "\e[92mLinking binary $@\e[0m"
$(CXX) $(OBJS) $(BASE_LDFLAGS) $($(CONFIG)_LDFLAGS) -o $@
diff --git a/speech2/src/main.cpp b/speech2/src/main.cpp
index 134d7b7..f7c3842 100644
--- a/speech2/src/main.cpp
+++ b/speech2/src/main.cpp
@@ -1,120 +1,40 @@
-#include
-#include
-
-#include
-#include
-#include
+#include
#include "speechapi.hpp"
-// args
-// -v (voice to use)
-// -s 4|5 (what SAPI version to use)
-// -p 0..100 (pitch)
-// -s 0..100
-// [message]
+#define SP2_EXPORT __declspec(dllexport)
-int main(int argc, char** argv) {
-#if 1
- if(FAILED(CoInitialize(nullptr))) {
- printf("Couldn't initalize COM\n");
- return 1;
+enum class EngineType : int { ET_SAPI4, ET_SAPI5, ET_DECTALK };
+
+extern "C" {
+
+SP2_EXPORT void* speech2_create_api(EngineType type) {
+ switch(type) {
+ case EngineType::ET_SAPI4: return static_cast(ISpeechAPI::CreateSapi4());
+ default: return nullptr;
}
-
- auto api = ISpeechAPI::CreateSapi4();
- if(!api) {
- printf("Couldn't allocate memory for speech API\n");
- return 1;
- }
-
- if(auto hRes = api->Initialize(); FAILED(hRes)) {
- printf("Couldn't initalize SAPI 4 (hr: %08x)\n", hRes);
- return 1;
- }
-
- #if 1
- auto voices = api->GetVoices();
- printf("Available voices:\n");
- for(auto& voice : voices) {
- auto& guid = voice.guid;
-
- 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,
- guid.Data4[0], guid.Data4[1], guid.Data4[2], guid.Data4[3], guid.Data4[4], guid.Data4[5], guid.Data4[6], guid.Data4[7]);
- }
- #endif
-
- if(auto hRes = api->SelectVoice("Sam"); FAILED(hRes)) {
- 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(ctx));
- return *static_cast(ctx) == 4;
- }, static_cast(&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(ctx));
- return *static_cast(ctx) == 4;
- }, static_cast(&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;
+}
+
+SP2_EXPORT void speech2_destroy_api(void* engine) {
+ if(engine)
+ delete static_cast(engine);
+}
+
+}
+
+BOOL WINAPI DllMain(HINSTANCE hinstDLL, DWORD fdwReason, LPVOID lpvReserved) {
+ switch(fdwReason) {
+ case DLL_PROCESS_ATTACH: break;
+
+ case DLL_THREAD_ATTACH: break;
+
+ case DLL_THREAD_DETACH: break;
+
+ case DLL_PROCESS_DETACH:
+ if(lpvReserved != nullptr) {
+ break; // do not do cleanup if process termination scenario
+ }
+ break;
+ }
+ return TRUE;
}