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; }