Compare commits
5 commits
20f59c5663
...
126566c3cf
Author | SHA1 | Date | |
---|---|---|---|
126566c3cf | |||
3b127a0b08 | |||
13ce046d3f | |||
1ebd3285f9 | |||
bd048875c8 |
26 changed files with 399 additions and 927 deletions
18
Justfile
18
Justfile
|
@ -1,7 +1,23 @@
|
|||
build:
|
||||
dotnet 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=Release -j$(nproc)
|
||||
|
||||
cp speech2/bin/x86/Release/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
|
||||
|
||||
|
||||
format:
|
||||
cd SAPIServer; for f in *.cs; do clang-format -i $f; done; cd ..;
|
||||
|
|
|
@ -3,112 +3,103 @@ using System.Collections.Generic;
|
|||
using System.Net;
|
||||
using System.Text;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace SAPIServer
|
||||
{
|
||||
/// <summary>
|
||||
/// Simple routing HTTP server for .NET Framework
|
||||
/// </summary>
|
||||
class HTTPServer
|
||||
{
|
||||
private bool listening = false;
|
||||
private HttpListener listener;
|
||||
private Dictionary<string, Func<HttpListenerContext, byte[]>> GETListeners;
|
||||
private Dictionary<string, Func<HttpListenerContext, byte[]>> POSTListeners;
|
||||
namespace SAPIServer {
|
||||
/// <summary>
|
||||
/// Simple routing HTTP server for .NET Framework
|
||||
/// </summary>
|
||||
class HTTPServer {
|
||||
private bool listening = false;
|
||||
private HttpListener listener;
|
||||
private Dictionary<string, Func<HttpListenerContext, byte[]>> GETListeners;
|
||||
private Dictionary<string, Func<HttpListenerContext, byte[]>> POSTListeners;
|
||||
|
||||
public HTTPServer()
|
||||
{
|
||||
listener = new HttpListener();
|
||||
this.GETListeners = new Dictionary<string, Func<HttpListenerContext, byte[]>>();
|
||||
this.POSTListeners = new Dictionary<string, Func<HttpListenerContext, byte[]>>();
|
||||
}
|
||||
public HTTPServer() {
|
||||
listener = new HttpListener();
|
||||
this.GETListeners = new Dictionary<string, Func<HttpListenerContext, byte[]>>();
|
||||
this.POSTListeners = new Dictionary<string, Func<HttpListenerContext, byte[]>>();
|
||||
}
|
||||
|
||||
public void Get(string uri, Func<HttpListenerContext, byte[]> 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<HttpListenerContext, byte[]> 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<HttpListenerContext, byte[]> 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<HttpListenerContext, byte[]> 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<string, Func<HttpListenerContext, byte[]>> 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<string, Func<HttpListenerContext, byte[]>> 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}");
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -6,69 +6,80 @@ 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 <port>");
|
||||
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<SynthesizePayload>(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 SpeechServer {
|
||||
private Dictionary<string, SpeechAPI> apis = new();
|
||||
private HTTPServer httpServer = new();
|
||||
|
||||
public SpeechServer() {
|
||||
// Test out creating a speech2 api object
|
||||
apis["sapi4"] = new SpeechAPI(EngineType.ET_SAPI4);
|
||||
#if true
|
||||
foreach(var voice in apis["sapi4"].GetVoices()) {
|
||||
Console.WriteLine($"ggg {voice.name}");
|
||||
}
|
||||
#endif
|
||||
|
||||
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() }));
|
||||
}*/
|
||||
|
||||
return new byte[]{0};
|
||||
});
|
||||
|
||||
httpServer.Post("/api/synthesize", ctx => {
|
||||
SynthesizePayload body;
|
||||
try {
|
||||
string bodyraw;
|
||||
using(var reader = new StreamReader(ctx.Request.InputStream)) {
|
||||
bodyraw = reader.ReadToEnd();
|
||||
}
|
||||
body = JsonConvert.DeserializeObject<SynthesizePayload>(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();
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public void Start(ushort port) {
|
||||
Console.WriteLine($"[{DateTime.Now:u}] Starting HTTP server on port {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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -6,10 +6,18 @@
|
|||
<GenerateAssemblyInfo>false</GenerateAssemblyInfo>
|
||||
<OutputType>Exe</OutputType>
|
||||
<TargetFrameworks>net40</TargetFrameworks>
|
||||
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
|
||||
</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>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
|
||||
<!--<PackageReference Include="Microsoft.Bcl.Async" Version="1.0.168" />-->
|
||||
<Reference Include="System" />
|
||||
<Reference Include="System.Core" />
|
||||
<Reference Include="System.Speech" />
|
||||
|
|
|
@ -1,84 +0,0 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<Project ToolsVersion="15.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
|
||||
<Import Project="$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props" Condition="Exists('$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props')" />
|
||||
<PropertyGroup>
|
||||
<Configuration Condition=" '$(Configuration)' == '' ">Debug</Configuration>
|
||||
<Platform Condition=" '$(Platform)' == '' ">AnyCPU</Platform>
|
||||
<ProjectGuid>{BF824074-4C4E-4DE1-8DCA-F022682B00E1}</ProjectGuid>
|
||||
<OutputType>Exe</OutputType>
|
||||
<RootNamespace>SAPIServer</RootNamespace>
|
||||
<AssemblyName>SAPIServer</AssemblyName>
|
||||
<TargetFrameworkVersion>v4.0</TargetFrameworkVersion>
|
||||
<FileAlignment>512</FileAlignment>
|
||||
<Deterministic>true</Deterministic>
|
||||
<PublishUrl>publish\</PublishUrl>
|
||||
<Install>true</Install>
|
||||
<InstallFrom>Disk</InstallFrom>
|
||||
<UpdateEnabled>false</UpdateEnabled>
|
||||
<UpdateMode>Foreground</UpdateMode>
|
||||
<UpdateInterval>7</UpdateInterval>
|
||||
<UpdateIntervalUnits>Days</UpdateIntervalUnits>
|
||||
<UpdatePeriodically>false</UpdatePeriodically>
|
||||
<UpdateRequired>false</UpdateRequired>
|
||||
<MapFileExtensions>true</MapFileExtensions>
|
||||
<ApplicationRevision>0</ApplicationRevision>
|
||||
<ApplicationVersion>1.0.0.%2a</ApplicationVersion>
|
||||
<IsWebBootstrapper>false</IsWebBootstrapper>
|
||||
<UseApplicationTrust>false</UseApplicationTrust>
|
||||
<BootstrapperEnabled>true</BootstrapperEnabled>
|
||||
</PropertyGroup>
|
||||
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' ">
|
||||
<PlatformTarget>AnyCPU</PlatformTarget>
|
||||
<DebugSymbols>true</DebugSymbols>
|
||||
<DebugType>full</DebugType>
|
||||
<Optimize>false</Optimize>
|
||||
<OutputPath>bin\Debug\</OutputPath>
|
||||
<DefineConstants>DEBUG;TRACE</DefineConstants>
|
||||
<ErrorReport>prompt</ErrorReport>
|
||||
<WarningLevel>4</WarningLevel>
|
||||
</PropertyGroup>
|
||||
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Release|AnyCPU' ">
|
||||
<PlatformTarget>AnyCPU</PlatformTarget>
|
||||
<DebugType>pdbonly</DebugType>
|
||||
<Optimize>true</Optimize>
|
||||
<OutputPath>bin\Release\</OutputPath>
|
||||
<DefineConstants>TRACE</DefineConstants>
|
||||
<ErrorReport>prompt</ErrorReport>
|
||||
<WarningLevel>4</WarningLevel>
|
||||
</PropertyGroup>
|
||||
<PropertyGroup>
|
||||
<ApplicationManifest>app.manifest</ApplicationManifest>
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<Reference Include="Newtonsoft.Json, Version=13.0.0.0, Culture=neutral, PublicKeyToken=30ad4fe6b2a6aeed, processorArchitecture=MSIL">
|
||||
<HintPath>..\packages\Newtonsoft.Json.13.0.3\lib\net40\Newtonsoft.Json.dll</HintPath>
|
||||
</Reference>
|
||||
<Reference Include="System" />
|
||||
<Reference Include="System.Core" />
|
||||
<Reference Include="System.Speech" />
|
||||
<Reference Include="System.Xml.Linq" />
|
||||
<Reference Include="System.Data.DataSetExtensions" />
|
||||
<Reference Include="Microsoft.CSharp" />
|
||||
<Reference Include="System.Data" />
|
||||
<Reference Include="System.Xml" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<Compile Include="HTTPServer.cs" />
|
||||
<Compile Include="Program.cs" />
|
||||
<Compile Include="Properties\AssemblyInfo.cs" />
|
||||
<Compile Include="SynthesizePayload.cs" />
|
||||
<Compile Include="VoicesResponse.cs" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<None Include="app.manifest" />
|
||||
<None Include="packages.config" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<BootstrapperPackage Include="Microsoft.Net.Framework.3.5.SP1">
|
||||
<Visible>False</Visible>
|
||||
<ProductName>.NET Framework 3.5 SP1</ProductName>
|
||||
<Install>false</Install>
|
||||
</BootstrapperPackage>
|
||||
</ItemGroup>
|
||||
<Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />
|
||||
</Project>
|
81
SAPIServer/SpeechDLL.cs
Normal file
81
SAPIServer/SpeechDLL.cs
Normal file
|
@ -0,0 +1,81 @@
|
|||
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Security;
|
||||
|
||||
namespace SAPIServer {
|
||||
|
||||
// Sync with C++ code.
|
||||
public enum EngineType : int { ET_SAPI4, ET_SAPI5, ET_DECTALK }
|
||||
|
||||
public class VoiceDef {
|
||||
public Guid guid;
|
||||
public string name;
|
||||
}
|
||||
|
||||
[StructLayout(LayoutKind.Sequential, Pack = 1)]
|
||||
internal struct VoiceDefInternal {
|
||||
Guid guid;
|
||||
|
||||
[MarshalAs(UnmanagedType.LPStr)]
|
||||
string name;
|
||||
|
||||
public VoiceDef Voicify() {
|
||||
Console.WriteLine($"FUCK ${name}");
|
||||
//var str = Marshal.PtrToStringAnsi(name);
|
||||
VoiceDef def = new();
|
||||
|
||||
def.guid = guid;
|
||||
def.name = name;
|
||||
return def;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Speech2 DLL API. Sync with c++ code.
|
||||
internal class SpeechDLL {
|
||||
[DllImport("speech2.dll")]
|
||||
public static extern IntPtr speech2_create_api(EngineType type);
|
||||
|
||||
[DllImport("speech2.dll")]
|
||||
public static extern void speech2_destroy_api(IntPtr pAPI);
|
||||
|
||||
[DllImport("speech2.dll")]
|
||||
public static extern int speech2_api_get_voiceinfo_count(IntPtr pAPI);
|
||||
|
||||
[DllImport("speech2.dll")]
|
||||
public static extern IntPtr speech2_api_get_voiceinfo_index(IntPtr pAPI, int index);
|
||||
}
|
||||
|
||||
// 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;
|
||||
|
||||
public SpeechAPI(EngineType type) {
|
||||
handle = SpeechDLL.speech2_create_api(type);
|
||||
if(handle == IntPtr.Zero)
|
||||
throw new InvalidOperationException("Failed to create speech API");
|
||||
}
|
||||
|
||||
public List<VoiceDef> GetVoices() {
|
||||
var count = SpeechDLL.speech2_api_get_voiceinfo_count(handle);
|
||||
Console.WriteLine($"count {count}");
|
||||
var list = new List<VoiceDef>();
|
||||
for(var i = 0; i < count; ++i) {
|
||||
var ptr = SpeechDLL.speech2_api_get_voiceinfo_index(handle, i);
|
||||
var obj = (VoiceDefInternal)Marshal.PtrToStructure(ptr, typeof(VoiceDefInternal));
|
||||
list.Add(obj.Voicify());
|
||||
}
|
||||
return list;
|
||||
}
|
||||
|
||||
void IDisposable.Dispose() {
|
||||
if(handle != IntPtr.Zero)
|
||||
SpeechDLL.speech2_destroy_api(handle);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -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; }
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,7 +1,5 @@
|
|||
namespace SAPIServer
|
||||
{
|
||||
class VoicesResponse
|
||||
{
|
||||
public string[] voices { get; set; }
|
||||
}
|
||||
namespace SAPIServer {
|
||||
class VoicesResponse {
|
||||
public string[] voices { get; set; }
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
include build/arch.mk
|
||||
include build/configs.mk
|
||||
|
||||
NAME = sapiserver
|
||||
NAME = speech2
|
||||
|
||||
BINDIR = bin/$(ARCH)/$(CONFIG)
|
||||
OBJDIR = obj/$(ARCH)/$(CONFIG)
|
||||
|
@ -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)/:
|
||||
|
|
|
@ -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-pic -fno-pie -fno-ident -msse -Iinclude -Isrc -D_UCRT -D_WIN32_WINNT=0x0501
|
||||
BASE_CXXFLAGS = -MMD -fvisibility=hidden -std=c++20 -fpermissive -fno-pic -fno-pie -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
|
||||
|
|
|
@ -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 $@
|
||||
|
||||
|
|
|
@ -1,156 +0,0 @@
|
|||
#include <handleapi.h>
|
||||
#include <synchapi.h>
|
||||
#include <windows.h>
|
||||
|
||||
#include <base/BThread.hpp>
|
||||
|
||||
namespace base::osdep {
|
||||
|
||||
enum class MemoryOrder {
|
||||
Relaxed = __ATOMIC_RELAXED,
|
||||
Consume = __ATOMIC_CONSUME,
|
||||
Acquire = __ATOMIC_ACQUIRE,
|
||||
Release = __ATOMIC_RELEASE,
|
||||
AcqRel = __ATOMIC_ACQ_REL,
|
||||
SeqCst = __ATOMIC_SEQ_CST
|
||||
};
|
||||
|
||||
// TODO: public!
|
||||
template<class T, MemoryOrder DefaultOrder = MemoryOrder::SeqCst>
|
||||
struct BAtomic {
|
||||
BAtomic() = default;
|
||||
BAtomic(const T value) :
|
||||
value(value) {
|
||||
|
||||
}
|
||||
|
||||
inline T fetch_add(T val, MemoryOrder order = DefaultOrder) volatile noexcept {
|
||||
return __atomic_fetch_add(&value, val, static_cast<int>(order));
|
||||
}
|
||||
|
||||
inline T fetch_sub(T val, MemoryOrder order = DefaultOrder) volatile noexcept {
|
||||
volatile T* ptr = &value;
|
||||
return __atomic_fetch_sub(ptr, val, static_cast<int>(order));
|
||||
}
|
||||
|
||||
void store(T desiredValue, MemoryOrder order = DefaultOrder) volatile noexcept {
|
||||
__atomic_store_n(&value, desiredValue, order);
|
||||
}
|
||||
|
||||
T operator++() volatile noexcept {
|
||||
return fetch_add(1) + 1;
|
||||
}
|
||||
|
||||
T operator++(int) volatile noexcept {
|
||||
return fetch_add(1);
|
||||
}
|
||||
|
||||
T operator--() volatile noexcept {
|
||||
return fetch_sub(1) - 1;
|
||||
}
|
||||
|
||||
T operator--(int) volatile noexcept {
|
||||
return fetch_sub(1);
|
||||
}
|
||||
|
||||
T operator-=(T val) volatile noexcept {
|
||||
return fetch_sub(val) - val;
|
||||
}
|
||||
|
||||
T operator+=(T val) volatile noexcept{
|
||||
return fetch_add(val) + val;
|
||||
}
|
||||
private:
|
||||
T value;
|
||||
};
|
||||
|
||||
struct BCondVar {
|
||||
BCondVar() {
|
||||
hNotifyAllEvent = CreateEventA(nullptr, TRUE, FALSE, nullptr);
|
||||
hNotifyOneEvent = CreateEventA(nullptr, FALSE, FALSE, nullptr);
|
||||
|
||||
waiterMutex = BMutex_Create(true);
|
||||
}
|
||||
|
||||
~BCondVar() {
|
||||
if(hNotifyAllEvent != INVALID_HANDLE_VALUE)
|
||||
CloseHandle(hNotifyAllEvent);
|
||||
if(hNotifyOneEvent != INVALID_HANDLE_VALUE)
|
||||
CloseHandle(hNotifyOneEvent);
|
||||
|
||||
BMutex_Destroy(waiterMutex);
|
||||
}
|
||||
|
||||
void SignalOne() {
|
||||
SetEvent(hNotifyOneEvent);
|
||||
}
|
||||
|
||||
void SignalAll() {
|
||||
SetEvent(hNotifyAllEvent);
|
||||
}
|
||||
|
||||
void Wait(bool(*predicate)(void* ctx), void* ctx) {
|
||||
HANDLE handles[2] = { hNotifyAllEvent, hNotifyOneEvent };
|
||||
|
||||
BMutex_Lock(waiterMutex);
|
||||
waiterSemaphore++;
|
||||
BMutex_Unlock(waiterMutex);
|
||||
|
||||
while(!predicate(ctx)) {
|
||||
switch(WaitForMultipleObjects(2, &handles[0], FALSE, INFINITE)) {
|
||||
|
||||
case WAIT_OBJECT_0: // hNotifyAllEvent
|
||||
BMutex_Lock(waiterMutex);
|
||||
if(waiterSemaphore-- == 0) {
|
||||
ResetEvent(hNotifyAllEvent);
|
||||
}
|
||||
BMutex_Unlock(waiterMutex);
|
||||
break;
|
||||
|
||||
case WAIT_OBJECT_0 + 1: // hNotifyOneEvent
|
||||
continue;
|
||||
break;
|
||||
|
||||
case WAIT_FAILED:
|
||||
return;
|
||||
break;
|
||||
|
||||
default:
|
||||
return;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
HANDLE hNotifyAllEvent{};
|
||||
HANDLE hNotifyOneEvent{};
|
||||
|
||||
// Semaphore for SignalAll().
|
||||
BMutex* waiterMutex;
|
||||
BAtomic<int> waiterSemaphore{0};
|
||||
};
|
||||
|
||||
BCondVar* BCondVar_Create() {
|
||||
return new BCondVar();
|
||||
}
|
||||
|
||||
/// Signals one thread.
|
||||
void BCondVar_SignalOne(BCondVar* cond) {
|
||||
cond->SignalOne();
|
||||
}
|
||||
|
||||
// Signals all threads.
|
||||
void BCondVar_SignalAll(BCondVar* cond) {
|
||||
cond->SignalAll();
|
||||
}
|
||||
|
||||
|
||||
// Waits. Call this on all threads.
|
||||
void BCondVar_Wait(BCondVar* condvar, bool(*predicate)(void* ctx), void* ctx) {
|
||||
condvar->Wait(predicate, ctx);
|
||||
}
|
||||
|
||||
void BCondVar_Destroy(BCondVar* condvar) {
|
||||
delete condvar;
|
||||
}
|
||||
}
|
|
@ -1,53 +0,0 @@
|
|||
#include <base/BThread.hpp>
|
||||
#include <cassert>
|
||||
|
||||
namespace base::osdep {
|
||||
|
||||
struct BMutex {
|
||||
CRITICAL_SECTION critSec {};
|
||||
bool recursive{};
|
||||
|
||||
BMutex(bool recursive = false) : recursive(recursive) { InitializeCriticalSection(&critSec); }
|
||||
|
||||
~BMutex() {
|
||||
if(critSec.LockCount != 0)
|
||||
Unlock();
|
||||
|
||||
DeleteCriticalSection(&critSec);
|
||||
}
|
||||
|
||||
inline void Lock() {
|
||||
|
||||
// recursive lock check
|
||||
if(!recursive) {
|
||||
if(critSec.LockCount + 1 > 1) {
|
||||
ExitProcess(0x69420);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
EnterCriticalSection(&critSec);
|
||||
}
|
||||
|
||||
inline void Unlock() { LeaveCriticalSection(&critSec); }
|
||||
};
|
||||
|
||||
BMutex* BMutex_Create(bool recursive) {
|
||||
return new BMutex(recursive);
|
||||
}
|
||||
|
||||
void BMutex_Destroy(BMutex* mutex) {
|
||||
delete mutex;
|
||||
}
|
||||
|
||||
void BMutex_Lock(BMutex* mutex) {
|
||||
if(mutex)
|
||||
mutex->Lock();
|
||||
}
|
||||
|
||||
void BMutex_Unlock(BMutex* mutex) {
|
||||
if(mutex)
|
||||
mutex->Unlock();
|
||||
}
|
||||
|
||||
} // namespace base::osdep
|
|
@ -1,38 +0,0 @@
|
|||
#include <process.h>
|
||||
#include <base/BThread.hpp>
|
||||
|
||||
namespace base::osdep {
|
||||
|
||||
struct BThread {
|
||||
HANDLE hThread;
|
||||
unsigned dwId;
|
||||
};
|
||||
|
||||
BThreadHandle BThread_Spawn(BThreadFunc ep, void* arg, unsigned stackSize) {
|
||||
unsigned dwID{};
|
||||
auto res = _beginthreadex(nullptr, stackSize, static_cast<_beginthreadex_proc_type>(ep), arg, 0, &dwID);
|
||||
|
||||
if(res == -1)
|
||||
return nullptr;
|
||||
|
||||
auto handle = new BThread();
|
||||
handle->hThread = reinterpret_cast<HANDLE>(res);
|
||||
handle->dwId = dwID;
|
||||
|
||||
return handle;
|
||||
}
|
||||
|
||||
unsigned BThread_GetID(BThreadHandle handle) {
|
||||
if(handle)
|
||||
return handle->dwId;
|
||||
|
||||
return -1;
|
||||
}
|
||||
|
||||
void BThread_Join(BThreadHandle handle) {
|
||||
if(handle) {
|
||||
auto res = WaitForSingleObject(handle->hThread, INFINITE);
|
||||
return;
|
||||
}
|
||||
}
|
||||
} // namespace base::osdep
|
|
@ -1,64 +0,0 @@
|
|||
// BThread - it's like GThread, but mentally sane!
|
||||
// (and without __, pthreads, and other smells.)
|
||||
#pragma once
|
||||
|
||||
#ifdef _WIN32
|
||||
#include <base/SaneWin.hpp>
|
||||
#else
|
||||
#error BThread only supports Windows.
|
||||
#endif
|
||||
|
||||
namespace base::osdep {
|
||||
|
||||
// Threads
|
||||
|
||||
struct BThread;
|
||||
|
||||
using BThreadHandle = BThread*;
|
||||
|
||||
#ifdef _WIN32
|
||||
using BThreadNativeHandle = HANDLE;
|
||||
#endif
|
||||
|
||||
using BThreadFunc =
|
||||
unsigned WINAPI (*)(void* argp);
|
||||
|
||||
// Spawns a new thread, with the given entry point, argument, and stack size.
|
||||
BThreadHandle BThread_Spawn(BThreadFunc ep, void* arg = nullptr, unsigned stackSize = 0);
|
||||
|
||||
// TODO BThread_Native(BThreadHandle handle)
|
||||
|
||||
unsigned BThread_GetID(BThreadHandle handle);
|
||||
|
||||
// Joins (waits for this thread to terminate) this thread.
|
||||
void BThread_Join(BThreadHandle handle);
|
||||
|
||||
// Mutexes
|
||||
|
||||
struct BMutex;
|
||||
|
||||
// if recursive is true, this BMutex will be a recursive mutex,
|
||||
// and multiple threads are allowed to lock it.
|
||||
BMutex* BMutex_Create(bool recursive);
|
||||
|
||||
void BMutex_Lock(BMutex* mutex);
|
||||
void BMutex_Unlock(BMutex* mutex);
|
||||
|
||||
void BMutex_Destroy(BMutex* mutex);
|
||||
|
||||
struct BCondVar;
|
||||
|
||||
BCondVar* BCondVar_Create();
|
||||
|
||||
/// Signals one thread.
|
||||
void BCondVar_SignalOne(BCondVar* cond);
|
||||
|
||||
// Signals all threads.
|
||||
void BCondVar_SignalAll(BCondVar* cond);
|
||||
|
||||
|
||||
// Waits. Call this on all threads.
|
||||
void BCondVar_Wait(BCondVar* condvar, bool(*predicate)(void* ctx), void* ctx);
|
||||
|
||||
void BCondVar_Destroy(BCondVar* condvar);
|
||||
}
|
|
@ -1,21 +0,0 @@
|
|||
#include <base/Mutex.hpp>
|
||||
|
||||
namespace base {
|
||||
|
||||
Mutex::Mutex() {
|
||||
mutex = osdep::BMutex_Create(false);
|
||||
}
|
||||
|
||||
Mutex::~Mutex() {
|
||||
osdep::BMutex_Destroy(mutex);
|
||||
}
|
||||
|
||||
void Mutex::Lock() {
|
||||
osdep::BMutex_Lock(mutex);
|
||||
}
|
||||
|
||||
void Mutex::Unlock() {
|
||||
osdep::BMutex_Unlock(mutex);
|
||||
}
|
||||
|
||||
} // namespace base
|
|
@ -1,47 +0,0 @@
|
|||
#pragma once
|
||||
#include <base/BThread.hpp>
|
||||
|
||||
namespace base {
|
||||
|
||||
/**
|
||||
* A mutex.
|
||||
*/
|
||||
struct Mutex {
|
||||
Mutex();
|
||||
~Mutex();
|
||||
|
||||
Mutex(const Mutex&) = delete;
|
||||
Mutex(Mutex&&) = default;
|
||||
|
||||
void Lock();
|
||||
void Unlock();
|
||||
|
||||
// impl data.
|
||||
private:
|
||||
osdep::BMutex* mutex;
|
||||
};
|
||||
|
||||
template <typename T>
|
||||
concept Lockable = requires(T t) {
|
||||
{ t.Lock() };
|
||||
{ t.Unlock() };
|
||||
};
|
||||
|
||||
/**
|
||||
* Scoped lock guard.
|
||||
*/
|
||||
template<Lockable Mut>
|
||||
struct LockGuard {
|
||||
LockGuard(Mut& mtx)
|
||||
: mutex(mtx) {
|
||||
mutex.Lock();
|
||||
}
|
||||
|
||||
~LockGuard() {
|
||||
mutex.Unlock();
|
||||
}
|
||||
private:
|
||||
Mut& mutex;
|
||||
};
|
||||
|
||||
}
|
|
@ -1,10 +0,0 @@
|
|||
# base/
|
||||
|
||||
This basically contains replacements of stuff from the standard library that we can't use on Windows XP because mingw sucks:
|
||||
|
||||
- Mutex
|
||||
- Thread
|
||||
- ManualResetEvent
|
||||
- AutoResetEvent
|
||||
|
||||
Oh and some stuff for dealing with COM
|
|
@ -1,53 +0,0 @@
|
|||
#include <base/Thread.hpp>
|
||||
|
||||
namespace base {
|
||||
|
||||
/*static*/ unsigned Thread::EntryFunc(void* argp) {
|
||||
auto invocable = static_cast<ThreadInvocable*>(argp);
|
||||
(*invocable)();
|
||||
|
||||
// Usually cross thread frees are a no-no, but the thread effectively
|
||||
// owns the invocable once it has been passed to it, so /shrug.
|
||||
delete invocable;
|
||||
return 0;
|
||||
}
|
||||
|
||||
Thread::~Thread() {
|
||||
// Join thread on destruction, unless
|
||||
// it has already been detached.
|
||||
if(Joinable())
|
||||
Join();
|
||||
}
|
||||
|
||||
unsigned Thread::Id() const {
|
||||
return osdep::BThread_GetID(threadHandle);
|
||||
}
|
||||
|
||||
bool Thread::Joinable() const {
|
||||
if(!threadHandle)
|
||||
return false;
|
||||
|
||||
return joinable;
|
||||
}
|
||||
|
||||
void Thread::Detach() {
|
||||
threadHandle = nullptr;
|
||||
joinable = false;
|
||||
}
|
||||
|
||||
void Thread::Join() {
|
||||
if(Joinable())
|
||||
osdep::BThread_Join(threadHandle);
|
||||
}
|
||||
|
||||
|
||||
void Thread::SpawnImpl(ThreadInvocable* pInvocable) {
|
||||
threadHandle = osdep::BThread_Spawn(&Thread::EntryFunc, static_cast<void*>(pInvocable), 0);
|
||||
if(threadHandle != nullptr)
|
||||
joinable = true;
|
||||
else {
|
||||
// Thread failed to create, delete the invocable so we don't leak memory.
|
||||
delete pInvocable;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,70 +0,0 @@
|
|||
#pragma once
|
||||
#include <base/BThread.hpp>
|
||||
#include <base/SaneWin.hpp>
|
||||
#include <tuple>
|
||||
|
||||
namespace base {
|
||||
|
||||
// TODO: Put this in a bits header.
|
||||
#define __BASE_FWD(T) static_cast<T&&>
|
||||
|
||||
/// A thread.
|
||||
struct Thread {
|
||||
using NativeHandle = osdep::BThreadHandle;
|
||||
|
||||
Thread() = default;
|
||||
|
||||
template <class Func, class... Args>
|
||||
explicit Thread(Func&& func, Args&&... args) {
|
||||
struct FuncInvocable final : ThreadInvocable {
|
||||
Func&& func;
|
||||
std::tuple<Args&&...> args;
|
||||
|
||||
constexpr FuncInvocable(Func&& func, Args&&... args) : func(__BASE_FWD(Func)(func)), args({ __BASE_FWD(Args)(args)... }) {}
|
||||
|
||||
constexpr void operator()() override {
|
||||
std::apply([&](auto&&... argt) { func(__BASE_FWD(Args)(argt)...); }, args);
|
||||
}
|
||||
};
|
||||
|
||||
SpawnImpl(new FuncInvocable(__BASE_FWD(Func)(func), __BASE_FWD(Args)(args)...));
|
||||
}
|
||||
|
||||
Thread(const Thread&) = delete;
|
||||
Thread(Thread&&) = default;
|
||||
|
||||
~Thread();
|
||||
|
||||
// TODO: Actually return a OS native thread handle, instead of a BThreads handle.
|
||||
NativeHandle Native() const { return threadHandle; }
|
||||
|
||||
unsigned Id() const;
|
||||
|
||||
bool Joinable() const;
|
||||
|
||||
// Detaches the native thread.
|
||||
// Once this function is called the thread
|
||||
// will no longer be joinable.
|
||||
void Detach();
|
||||
|
||||
void Join();
|
||||
|
||||
private:
|
||||
// For type erasure. I know it's bad or whatever, but generally,
|
||||
// it shouldn't be a big enough deal.
|
||||
struct ThreadInvocable {
|
||||
virtual ~ThreadInvocable() = default;
|
||||
virtual void operator()() = 0;
|
||||
};
|
||||
|
||||
// Takes the invocable and spawns le epic heckin thread.
|
||||
void SpawnImpl(ThreadInvocable* pInvocable);
|
||||
|
||||
// Actually recieves a pointer to a [ThreadInvocable] on the heap,
|
||||
// synthesized from a given function.
|
||||
static unsigned WINAPI EntryFunc(void* args);
|
||||
|
||||
NativeHandle threadHandle {};
|
||||
bool joinable { false }; // implicitly false if there's no thread.
|
||||
};
|
||||
} // namespace base
|
|
@ -1,17 +0,0 @@
|
|||
#pragma once
|
||||
|
||||
#include <variant>
|
||||
#include <windows.h>
|
||||
|
||||
template<class T>
|
||||
struct ComResult {
|
||||
|
||||
private:
|
||||
|
||||
using VariantType = std::variant<
|
||||
HRESULT,
|
||||
T
|
||||
>;
|
||||
|
||||
VariantType storage;
|
||||
};
|
51
speech2/src/bindings.cpp
Normal file
51
speech2/src/bindings.cpp
Normal file
|
@ -0,0 +1,51 @@
|
|||
#include <windows.h>
|
||||
#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" {
|
||||
|
||||
SP2_EXPORT void* speech2_create_api(EngineType type) {
|
||||
ISpeechAPI* api = nullptr;
|
||||
switch(type) {
|
||||
case EngineType::ET_SAPI4:
|
||||
api = ISpeechAPI::CreateSapi4();
|
||||
break;
|
||||
default: return nullptr;
|
||||
}
|
||||
|
||||
if(auto hr = api->Initialize(); FAILED(hr)) {
|
||||
delete api;
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
return static_cast<void*>(api);
|
||||
}
|
||||
|
||||
SP2_EXPORT void speech2_destroy_api(void* engine) {
|
||||
if(engine)
|
||||
delete static_cast<ISpeechAPI*>(engine);
|
||||
}
|
||||
|
||||
// API bindings TODO
|
||||
|
||||
SP2_EXPORT int speech2_api_get_voiceinfo_count(void* engine) {
|
||||
if(engine) {
|
||||
auto* api = static_cast<ISpeechAPI*>(engine);
|
||||
return api->GetVoices().size();
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
SP2_EXPORT const ISpeechAPI::VoiceInfo* speech2_api_get_voiceinfo_index(void* engine, int index) {
|
||||
if(engine) {
|
||||
auto* api = static_cast<ISpeechAPI*>(engine);
|
||||
return &api->GetVoices()[index];
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
}
|
25
speech2/src/dllmain.cpp
Normal file
25
speech2/src/dllmain.cpp
Normal file
|
@ -0,0 +1,25 @@
|
|||
#include <windows.h>
|
||||
#include <winscard.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:
|
||||
CoInitialize(nullptr);
|
||||
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
|
||||
}
|
||||
CoUninitialize();
|
||||
break;
|
||||
}
|
||||
return TRUE;
|
||||
}
|
|
@ -1,120 +0,0 @@
|
|||
#include <stdio.h>
|
||||
#include <winscard.h>
|
||||
|
||||
#include <base/BThread.hpp>
|
||||
#include <base/Mutex.hpp>
|
||||
#include <base/Thread.hpp>
|
||||
|
||||
#include "speechapi.hpp"
|
||||
|
||||
// args
|
||||
// -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) {
|
||||
#if 1
|
||||
if(FAILED(CoInitialize(nullptr))) {
|
||||
printf("Couldn't initalize COM\n");
|
||||
return 1;
|
||||
}
|
||||
|
||||
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<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;
|
||||
}
|
|
@ -19,23 +19,40 @@ struct SpeechAPI_SAPI4 : public ISpeechAPI {
|
|||
HRESULT Initialize() override {
|
||||
HRESULT hRes;
|
||||
|
||||
printf("speech2: SpeechAPI_Sapi4::Initalize() begin\n");
|
||||
|
||||
hRes = pEnum.CreateInstance(CLSID_TTSEnumerator, CLSCTX_INPROC);
|
||||
if(FAILED(hRes))
|
||||
return hRes;
|
||||
|
||||
|
||||
pEnum->Reset();
|
||||
|
||||
printf("speech2: SpeechAPI_Sapi4::Initalize() created enum\n");
|
||||
|
||||
// Fill out voices
|
||||
EnumVoices();
|
||||
|
||||
|
||||
printf("speech2: SpeechAPI_Sapi4::Initalize() filled out voices! Yay\n");
|
||||
|
||||
return S_OK;
|
||||
}
|
||||
|
||||
std::vector<VoiceInfo> GetVoices() override {
|
||||
TTSMODEINFO found {};
|
||||
std::vector<VoiceInfo> ret;
|
||||
void EnumVoices() {
|
||||
static TTSMODEINFO found {};
|
||||
|
||||
while(!pEnum->Next(1, &found, nullptr)) {
|
||||
ret.push_back({ .guid = found.gModeID, .voiceName = found.szModeName });
|
||||
//auto ptr = strdup(found.szModeName);
|
||||
printf("EnumVoices() voice %p\n", &found.szModeName);
|
||||
//voices.push_back(VoiceInfo { .guid = found.gModeID, .voiceName = ptr });
|
||||
}
|
||||
|
||||
pEnum->Reset();
|
||||
return ret;
|
||||
}
|
||||
|
||||
const std::vector<VoiceInfo>& GetVoices() override {
|
||||
return voices;
|
||||
}
|
||||
|
||||
HRESULT SelectVoiceImpl(const GUID& guid) {
|
||||
|
@ -77,6 +94,8 @@ struct SpeechAPI_SAPI4 : public ISpeechAPI {
|
|||
|
||||
// The above comment is also why this isn't a ComPtr.
|
||||
AudioOutBuffer* pAudioOut { nullptr };
|
||||
|
||||
std::vector<VoiceInfo> voices{};
|
||||
};
|
||||
|
||||
ISpeechAPI* ISpeechAPI::CreateSapi4() {
|
||||
|
|
|
@ -7,7 +7,15 @@ struct ISpeechAPI {
|
|||
|
||||
struct VoiceInfo {
|
||||
GUID guid{}; // Optional. May not be filled out if th e
|
||||
std::string voiceName;
|
||||
char* voiceName;
|
||||
|
||||
//VoiceInfo(const VoiceInfo&) = delete;
|
||||
//VoiceInfo(VoiceInfo&&) = delete;
|
||||
|
||||
~VoiceInfo() {
|
||||
if(voiceName)
|
||||
free(voiceName);
|
||||
}
|
||||
};
|
||||
|
||||
virtual ~ISpeechAPI() = default;
|
||||
|
@ -19,7 +27,7 @@ struct ISpeechAPI {
|
|||
/// Performs the bare level of initalization required to use the speech API
|
||||
virtual HRESULT Initialize() = 0;
|
||||
|
||||
virtual std::vector<VoiceInfo> GetVoices() = 0;
|
||||
virtual const std::vector<VoiceInfo>& GetVoices() = 0;
|
||||
|
||||
/// Selects a voice.
|
||||
virtual HRESULT SelectVoice(std::string_view voiceName) = 0;
|
||||
|
|
Loading…
Reference in a new issue