actually use bindings

about time
This commit is contained in:
Lily Tsuru 2024-07-18 05:05:35 -04:00
parent 1ebd3285f9
commit 13ce046d3f
7 changed files with 86 additions and 45 deletions

View file

@ -2,10 +2,18 @@ build:
dotnet build -c Release dotnet build -c Release
make -C speech2 -j$(nproc) 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: build-debug:
dotnet build -c Debug dotnet build -c Debug
make -C speech2 CONFIG=Debug -j$(nproc) 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: clean:
rm -rf SAPIServer/bin SAPIServer/obj rm -rf SAPIServer/bin SAPIServer/obj
make -C speech2 clean make -C speech2 clean

View file

@ -7,22 +7,24 @@ using System.Text;
using Newtonsoft.Json; using Newtonsoft.Json;
namespace SAPIServer { namespace SAPIServer {
class Program {
static void Main(string[] args) { class SpeechServer {
if(args.Length < 1 || !ushort.TryParse(args[0], out var port)) { private Dictionary<string, SpeechAPI> apis = new();
Console.Error.WriteLine("Usage: SAPIServer.exe <port>"); private HTTPServer httpServer = new();
Environment.Exit(1);
return; public SpeechServer() {
} // Test out creating a speech2 api object
var http = new HTTPServer(); apis["sapi4"] = new SpeechAPI(EngineType.ET_SAPI4);
http.Get("/api/voices", ctx => {
httpServer.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( return Encoding.UTF8.GetBytes(
JsonConvert.SerializeObject(new VoicesResponse { voices = synth.GetInstalledVoices().Select(v => v.VoiceInfo.Name).ToArray() })); 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; SynthesizePayload body;
try { try {
string bodyraw; string bodyraw;
@ -38,20 +40,38 @@ namespace SAPIServer {
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 synth = new SpeechSynthesizer()) { using(var ms = new MemoryStream()) {
if(!synth.GetInstalledVoices().Any(v => v.VoiceInfo.Name == body.voice)) { using(var synth = new SpeechSynthesizer()) {
ctx.Response.StatusCode = 400; if(!synth.GetInstalledVoices().Any(v => v.VoiceInfo.Name == body.voice)) {
return Encoding.UTF8.GetBytes("Voice not found"); 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}"); 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 <port>");
Environment.Exit(1);
return;
}
var server = new SpeechServer();
server.Start(port);
} }
} }
} }

View file

@ -6,11 +6,18 @@
<GenerateAssemblyInfo>false</GenerateAssemblyInfo> <GenerateAssemblyInfo>false</GenerateAssemblyInfo>
<OutputType>Exe</OutputType> <OutputType>Exe</OutputType>
<TargetFrameworks>net40</TargetFrameworks> <TargetFrameworks>net40</TargetFrameworks>
</PropertyGroup>
<PropertyGroup>
<RuntimeIdentifier>windows-x86</RuntimeIdentifier>
<!-- N.B: This is only to gain support for some compiler-supported nicities, like new(). -->
<LangVersion>9.0</LangVersion>
</PropertyGroup> </PropertyGroup>
<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" /> <!--<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" />

View file

@ -8,17 +8,17 @@ namespace SAPIServer {
// Sync with C++ code. // Sync with C++ code.
public enum EngineType : int { ET_SAPI4, ET_SAPI5, ET_DECTALK } public enum EngineType : int { ET_SAPI4, ET_SAPI5, ET_DECTALK }
// Speech2 DLL API. // Speech2 DLL API. Sync with c++ code.
internal class SpeechDLL { internal class SpeechDLL {
[DllImport("speech2")] [DllImport("speech2.dll")]
public static extern IntPtr speech2_create_api(EngineType type); public static extern IntPtr speech2_create_api(EngineType type);
[DllImport("speech2.dll")]
[DllImport("speech2")]
public static extern void speech2_destroy_api(IntPtr pAPI); 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 { public class SpeechAPI : IDisposable {
private IntPtr handle = IntPtr.Zero; private IntPtr handle = IntPtr.Zero;

View file

@ -1,7 +1,7 @@
include build/arch.mk include build/arch.mk
include build/configs.mk include build/configs.mk
NAME = sapiserver NAME = speech2
BINDIR = bin/$(ARCH)/$(CONFIG) BINDIR = bin/$(ARCH)/$(CONFIG)
OBJDIR = obj/$(ARCH)/$(CONFIG) OBJDIR = obj/$(ARCH)/$(CONFIG)

View file

@ -1,9 +1,9 @@
#include <windows.h> #include <windows.h>
#include "speechapi.hpp" #include "speechapi.hpp"
#define SP2_EXPORT __declspec(dllexport) #define SP2_EXPORT __declspec(dllexport)
// Engine type. Sync with C#
enum class EngineType : int { ET_SAPI4, ET_SAPI5, ET_DECTALK }; enum class EngineType : int { ET_SAPI4, ET_SAPI5, ET_DECTALK };
extern "C" { extern "C" {
@ -20,21 +20,6 @@ SP2_EXPORT void speech2_destroy_api(void* engine) {
delete static_cast<ISpeechAPI*>(engine); delete static_cast<ISpeechAPI*>(engine);
} }
} // API bindings TODO
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;
} }

21
speech2/src/dllmain.cpp Normal file
View file

@ -0,0 +1,21 @@
#include <windows.h>
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;
}