diff --git a/Justfile b/Justfile index 4b176d2..4752347 100644 --- a/Justfile +++ b/Justfile @@ -2,10 +2,18 @@ build: dotnet build -c Release make -C speech2 -j$(nproc) + cp speech2/bin/x86/Release/speech2.dll SAPIServer/bin/Release/net40/windows-x86 + cp /usr/i686-w64-mingw32/bin/libgcc_s_dw2-1.dll SAPIServer/bin/Release/net40/windows-x86/ + cp /usr/i686-w64-mingw32/bin/libstdc++-6.dll SAPIServer/bin/Release/net40/windows-x86/ + build-debug: dotnet build -c Debug make -C speech2 CONFIG=Debug -j$(nproc) + cp speech2/bin/x86/Debug/speech2.dll SAPIServer/bin/Debug/net40/windows-x86 + cp /usr/i686-w64-mingw32/bin/libgcc_s_dw2-1.dll SAPIServer/bin/Debug/net40/windows-x86/ + cp /usr/i686-w64-mingw32/bin/libstdc++-6.dll SAPIServer/bin/Debug/net40/windows-x86/ + clean: rm -rf SAPIServer/bin SAPIServer/obj make -C speech2 clean diff --git a/SAPIServer/Program.cs b/SAPIServer/Program.cs index 06c2a97..29deb43 100644 --- a/SAPIServer/Program.cs +++ b/SAPIServer/Program.cs @@ -7,22 +7,24 @@ 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 => { + + class SpeechServer { + private Dictionary apis = new(); + private HTTPServer httpServer = new(); + + public SpeechServer() { + // Test out creating a speech2 api object + apis["sapi4"] = new SpeechAPI(EngineType.ET_SAPI4); + + httpServer.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 => { + + httpServer.Post("/api/synthesize", ctx => { SynthesizePayload body; try { string bodyraw; @@ -38,20 +40,38 @@ namespace SAPIServer { 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"); + 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(); } - synth.SelectVoice(body.voice); - synth.SetOutputToWaveStream(ms); - synth.Speak(body.text); - ctx.Response.ContentType = "audio/wav"; - return ms.ToArray(); } }); + } + + public void Start(ushort port) { Console.WriteLine($"[{DateTime.Now:u}] Starting HTTP server on port {port}"); - http.Listen(port); + httpServer.Listen(port); + } + } + + 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 server = new SpeechServer(); + server.Start(port); } } } diff --git a/SAPIServer/SAPIServer.csproj b/SAPIServer/SAPIServer.csproj index 5a75f0b..68058e6 100644 --- a/SAPIServer/SAPIServer.csproj +++ b/SAPIServer/SAPIServer.csproj @@ -6,11 +6,18 @@ false Exe net40 + + + + + windows-x86 + + 9.0 - + diff --git a/SAPIServer/SpeechDLL.cs b/SAPIServer/SpeechDLL.cs index f306a93..29674f1 100644 --- a/SAPIServer/SpeechDLL.cs +++ b/SAPIServer/SpeechDLL.cs @@ -8,17 +8,17 @@ namespace SAPIServer { // Sync with C++ code. public enum EngineType : int { ET_SAPI4, ET_SAPI5, ET_DECTALK } - // Speech2 DLL API. + // Speech2 DLL API. Sync with c++ code. internal class SpeechDLL { - [DllImport("speech2")] + [DllImport("speech2.dll")] public static extern IntPtr speech2_create_api(EngineType type); - - [DllImport("speech2")] + [DllImport("speech2.dll")] public static extern void speech2_destroy_api(IntPtr pAPI); } - // Native binding of speech2 library from C++ to C#. + // A speech API. This is generic for all speech2 supported speech APIs, so + // we can use the same code for everything. Cool, huh? public class SpeechAPI : IDisposable { private IntPtr handle = IntPtr.Zero; diff --git a/speech2/Makefile b/speech2/Makefile index 81eefa7..346e722 100644 --- a/speech2/Makefile +++ b/speech2/Makefile @@ -1,7 +1,7 @@ include build/arch.mk include build/configs.mk -NAME = sapiserver +NAME = speech2 BINDIR = bin/$(ARCH)/$(CONFIG) OBJDIR = obj/$(ARCH)/$(CONFIG) diff --git a/speech2/src/main.cpp b/speech2/src/bindings.cpp similarity index 56% rename from speech2/src/main.cpp rename to speech2/src/bindings.cpp index f7c3842..c28efc9 100644 --- a/speech2/src/main.cpp +++ b/speech2/src/bindings.cpp @@ -1,9 +1,9 @@ #include - #include "speechapi.hpp" #define SP2_EXPORT __declspec(dllexport) +// Engine type. Sync with C# enum class EngineType : int { ET_SAPI4, ET_SAPI5, ET_DECTALK }; extern "C" { @@ -20,21 +20,6 @@ SP2_EXPORT void speech2_destroy_api(void* 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; +// API bindings TODO + } diff --git a/speech2/src/dllmain.cpp b/speech2/src/dllmain.cpp new file mode 100644 index 0000000..129073d --- /dev/null +++ b/speech2/src/dllmain.cpp @@ -0,0 +1,21 @@ +#include + +BOOL WINAPI DllMain(HINSTANCE hinstDLL, DWORD fdwReason, LPVOID lpvReserved) { + // N.B: Should initalize COM if it's not initalized. + // Note that with .NET Framework, *all* managed threads (incl. ThreadPool threads) + // have COM initalized by default, so we don't need to do so there. + 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; +}