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:
|
build:
|
||||||
dotnet build
|
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:
|
||||||
|
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:
|
clean:
|
||||||
rm -rf SAPIServer/bin SAPIServer/obj
|
rm -rf SAPIServer/bin SAPIServer/obj
|
||||||
make -C speech2 clean
|
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.Net;
|
||||||
using System.Text;
|
using System.Text;
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
namespace SAPIServer
|
namespace SAPIServer {
|
||||||
{
|
/// <summary>
|
||||||
/// <summary>
|
/// Simple routing HTTP server for .NET Framework
|
||||||
/// Simple routing HTTP server for .NET Framework
|
/// </summary>
|
||||||
/// </summary>
|
class HTTPServer {
|
||||||
class HTTPServer
|
private bool listening = false;
|
||||||
{
|
private HttpListener listener;
|
||||||
private bool listening = false;
|
private Dictionary<string, Func<HttpListenerContext, byte[]>> GETListeners;
|
||||||
private HttpListener listener;
|
private Dictionary<string, Func<HttpListenerContext, byte[]>> POSTListeners;
|
||||||
private Dictionary<string, Func<HttpListenerContext, byte[]>> GETListeners;
|
|
||||||
private Dictionary<string, Func<HttpListenerContext, byte[]>> POSTListeners;
|
|
||||||
|
|
||||||
public HTTPServer()
|
public HTTPServer() {
|
||||||
{
|
listener = new HttpListener();
|
||||||
listener = new HttpListener();
|
this.GETListeners = new Dictionary<string, Func<HttpListenerContext, byte[]>>();
|
||||||
this.GETListeners = new Dictionary<string, Func<HttpListenerContext, byte[]>>();
|
this.POSTListeners = new Dictionary<string, Func<HttpListenerContext, byte[]>>();
|
||||||
this.POSTListeners = new Dictionary<string, Func<HttpListenerContext, byte[]>>();
|
}
|
||||||
}
|
|
||||||
|
|
||||||
public void Get(string uri, Func<HttpListenerContext, byte[]> handler)
|
public void Get(string uri, Func<HttpListenerContext, byte[]> handler) {
|
||||||
{
|
if(GETListeners.ContainsKey(uri))
|
||||||
if (GETListeners.ContainsKey(uri)) throw new InvalidOperationException("A handler by that URI already exists.");
|
throw new InvalidOperationException("A handler by that URI already exists.");
|
||||||
if (!uri.StartsWith("/")) throw new ArgumentException("URIs must start with /");
|
if(!uri.StartsWith("/"))
|
||||||
this.GETListeners.Add(uri, handler);
|
throw new ArgumentException("URIs must start with /");
|
||||||
}
|
this.GETListeners.Add(uri, handler);
|
||||||
|
}
|
||||||
|
|
||||||
public void Post(string uri, Func<HttpListenerContext, byte[]> handler)
|
public void Post(string uri, Func<HttpListenerContext, byte[]> handler) {
|
||||||
{
|
if(POSTListeners.ContainsKey(uri))
|
||||||
if (POSTListeners.ContainsKey(uri)) throw new InvalidOperationException("A handler by that URI already exists.");
|
throw new InvalidOperationException("A handler by that URI already exists.");
|
||||||
if (!uri.StartsWith("/")) throw new ArgumentException("URIs must start with /");
|
if(!uri.StartsWith("/"))
|
||||||
this.POSTListeners.Add(uri, handler);
|
throw new ArgumentException("URIs must start with /");
|
||||||
}
|
this.POSTListeners.Add(uri, handler);
|
||||||
|
}
|
||||||
|
|
||||||
public void Listen(ushort port)
|
public void Listen(ushort port) {
|
||||||
{
|
if(listening)
|
||||||
if (listening) throw new InvalidOperationException("This HTTPServer is already listening.");
|
throw new InvalidOperationException("This HTTPServer is already listening.");
|
||||||
listening = true;
|
listening = true;
|
||||||
listener.Prefixes.Add(string.Format("http://*:{0}/", port));
|
listener.Prefixes.Add(string.Format("http://*:{0}/", port));
|
||||||
listener.Start();
|
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}");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
context.Response.ContentLength64 = response.Length;
|
while(listener.IsListening) {
|
||||||
context.Response.OutputStream.Write(response, 0, response.Length);
|
var context = listener.GetContext();
|
||||||
context.Response.OutputStream.Close();
|
ThreadPool.QueueUserWorkItem(
|
||||||
Console.WriteLine($"[{DateTime.Now:u}] {ip} - {context.Request.HttpMethod} {context.Request.RawUrl} - {context.Response.StatusCode}");
|
_ => {
|
||||||
} catch (Exception e)
|
var uri = context.Request.RawUrl.Split('?')[0];
|
||||||
{
|
var ip = context.Request.RemoteEndPoint.Address;
|
||||||
Console.Error.WriteLine($"[{DateTime.Now:u}] {ip} - {context.Request.HttpMethod} {context.Request.RawUrl} - Exception: {e.Message}");
|
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 System.Text;
|
||||||
using Newtonsoft.Json;
|
using Newtonsoft.Json;
|
||||||
|
|
||||||
namespace SAPIServer
|
namespace SAPIServer {
|
||||||
{
|
|
||||||
class Program
|
class SpeechServer {
|
||||||
{
|
private Dictionary<string, SpeechAPI> apis = new();
|
||||||
static void Main(string[] args)
|
private HTTPServer httpServer = new();
|
||||||
{
|
|
||||||
if (args.Length < 1 || !ushort.TryParse(args[0], out var port))
|
public SpeechServer() {
|
||||||
{
|
// Test out creating a speech2 api object
|
||||||
Console.Error.WriteLine("Usage: SAPIServer.exe <port>");
|
apis["sapi4"] = new SpeechAPI(EngineType.ET_SAPI4);
|
||||||
Environment.Exit(1);
|
#if true
|
||||||
return;
|
foreach(var voice in apis["sapi4"].GetVoices()) {
|
||||||
}
|
Console.WriteLine($"ggg {voice.name}");
|
||||||
var http = new HTTPServer();
|
}
|
||||||
http.Get("/api/voices", ctx =>
|
#endif
|
||||||
{
|
|
||||||
using (var synth = new SpeechSynthesizer())
|
httpServer.Get("/api/voices", ctx => {
|
||||||
{
|
/*
|
||||||
ctx.Response.ContentType = "application/json";
|
using(var synth = new SpeechSynthesizer()) {
|
||||||
return Encoding.UTF8.GetBytes(JsonConvert.SerializeObject(new VoicesResponse
|
ctx.Response.ContentType = "application/json";
|
||||||
{
|
return Encoding.UTF8.GetBytes(
|
||||||
voices = synth.GetInstalledVoices().Select(v => v.VoiceInfo.Name).ToArray()
|
JsonConvert.SerializeObject(new VoicesResponse { voices = synth.GetInstalledVoices().Select(v => v.VoiceInfo.Name).ToArray() }));
|
||||||
}));
|
}*/
|
||||||
}
|
|
||||||
});
|
return new byte[]{0};
|
||||||
http.Post("/api/synthesize", ctx =>
|
});
|
||||||
{
|
|
||||||
SynthesizePayload body;
|
httpServer.Post("/api/synthesize", ctx => {
|
||||||
try
|
SynthesizePayload body;
|
||||||
{
|
try {
|
||||||
string bodyraw;
|
string bodyraw;
|
||||||
using (var reader = new StreamReader(ctx.Request.InputStream))
|
using(var reader = new StreamReader(ctx.Request.InputStream)) {
|
||||||
{
|
bodyraw = reader.ReadToEnd();
|
||||||
bodyraw = reader.ReadToEnd();
|
}
|
||||||
}
|
body = JsonConvert.DeserializeObject<SynthesizePayload>(bodyraw);
|
||||||
body = JsonConvert.DeserializeObject<SynthesizePayload>(bodyraw);
|
} catch(Exception) {
|
||||||
}
|
ctx.Response.StatusCode = 400;
|
||||||
catch (Exception e)
|
return Encoding.UTF8.GetBytes("Bad payload");
|
||||||
{
|
}
|
||||||
ctx.Response.StatusCode = 400;
|
if(body == null || body.text == null || body.voice == null) {
|
||||||
return Encoding.UTF8.GetBytes("Bad payload");
|
ctx.Response.StatusCode = 400;
|
||||||
}
|
return Encoding.UTF8.GetBytes("Bad payload");
|
||||||
if (body == null || body.text == null || body.voice == null)
|
}
|
||||||
{
|
using(var ms = new MemoryStream()) {
|
||||||
ctx.Response.StatusCode = 400;
|
using(var synth = new SpeechSynthesizer()) {
|
||||||
return Encoding.UTF8.GetBytes("Bad payload");
|
if(!synth.GetInstalledVoices().Any(v => v.VoiceInfo.Name == body.voice)) {
|
||||||
}
|
ctx.Response.StatusCode = 400;
|
||||||
using (var ms = new MemoryStream())
|
return Encoding.UTF8.GetBytes("Voice not found");
|
||||||
using (var synth = new SpeechSynthesizer())
|
}
|
||||||
{
|
synth.SelectVoice(body.voice);
|
||||||
if (!synth.GetInstalledVoices().Any(v => v.VoiceInfo.Name == body.voice))
|
synth.SetOutputToWaveStream(ms);
|
||||||
{
|
synth.Speak(body.text);
|
||||||
ctx.Response.StatusCode = 400;
|
ctx.Response.ContentType = "audio/wav";
|
||||||
return Encoding.UTF8.GetBytes("Voice not found");
|
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}");
|
||||||
});
|
httpServer.Listen(port);
|
||||||
Console.WriteLine($"[{ DateTime.Now:u}] Starting HTTP server on port {port}");
|
}
|
||||||
http.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>
|
<GenerateAssemblyInfo>false</GenerateAssemblyInfo>
|
||||||
<OutputType>Exe</OutputType>
|
<OutputType>Exe</OutputType>
|
||||||
<TargetFrameworks>net40</TargetFrameworks>
|
<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>
|
</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" />-->
|
||||||
<Reference Include="System" />
|
<Reference Include="System" />
|
||||||
<Reference Include="System.Core" />
|
<Reference Include="System.Core" />
|
||||||
<Reference Include="System.Speech" />
|
<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
|
namespace SAPIServer {
|
||||||
{
|
class SynthesizePayload {
|
||||||
class SynthesizePayload
|
public string voice { get; set; }
|
||||||
{
|
public string text { get; set; }
|
||||||
public string voice { get; set; }
|
}
|
||||||
public string text { get; set; }
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,7 +1,5 @@
|
||||||
namespace SAPIServer
|
namespace SAPIServer {
|
||||||
{
|
class VoicesResponse {
|
||||||
class VoicesResponse
|
public string[] voices { get; set; }
|
||||||
{
|
}
|
||||||
public string[] voices { get; set; }
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -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)
|
||||||
|
@ -13,7 +13,7 @@ OBJS = $(addprefix $(OBJDIR)/,$(notdir $(CXXSRCS:.cpp=.o)))
|
||||||
|
|
||||||
.PHONY: all dumpinfo clean matrix
|
.PHONY: all dumpinfo clean matrix
|
||||||
|
|
||||||
all: $(BINDIR)/$(NAME).exe
|
all: $(BINDIR)/$(NAME).dll
|
||||||
|
|
||||||
# dir rules
|
# dir rules
|
||||||
$(BINDIR)/:
|
$(BINDIR)/:
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
# Base compiler flags. Only change if you *explicitly* know what you're doing.
|
# 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_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 -std=c++20 -fpermissive -fno-pic -fno-pie -fno-rtti -msse -Iinclude -Isrc -Ithird_party -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 = -mwindows -static -static-libgcc -lkernel32 -lshell32 -luser32 -luuid -lole32
|
BASE_LDFLAGS = -Wl,--subsystem=windows -fvisibility=hidden -shared -lkernel32 -lshell32 -luser32 -luuid -lole32
|
||||||
|
|
||||||
Release_Valid = yes
|
Release_Valid = yes
|
||||||
Release_CCFLAGS = -O3 -ffast-math -fomit-frame-pointer -DNDEBUG
|
Release_CCFLAGS = -O3 -ffast-math -fomit-frame-pointer -DNDEBUG
|
||||||
|
|
|
@ -1,5 +1,4 @@
|
||||||
# TODO: Link DLL
|
$(BINDIR)/$(NAME).dll: $(BINDIR)/ $(OBJDIR)/ $(OBJS)
|
||||||
$(BINDIR)/$(NAME).exe: $(BINDIR)/ $(OBJDIR)/ $(OBJS)
|
|
||||||
echo -e "\e[92mLinking binary $@\e[0m"
|
echo -e "\e[92mLinking binary $@\e[0m"
|
||||||
$(CXX) $(OBJS) $(BASE_LDFLAGS) $($(CONFIG)_LDFLAGS) -o $@
|
$(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 Initialize() override {
|
||||||
HRESULT hRes;
|
HRESULT hRes;
|
||||||
|
|
||||||
|
printf("speech2: SpeechAPI_Sapi4::Initalize() begin\n");
|
||||||
|
|
||||||
hRes = pEnum.CreateInstance(CLSID_TTSEnumerator, CLSCTX_INPROC);
|
hRes = pEnum.CreateInstance(CLSID_TTSEnumerator, CLSCTX_INPROC);
|
||||||
if(FAILED(hRes))
|
if(FAILED(hRes))
|
||||||
return 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;
|
return S_OK;
|
||||||
}
|
}
|
||||||
|
|
||||||
std::vector<VoiceInfo> GetVoices() override {
|
void EnumVoices() {
|
||||||
TTSMODEINFO found {};
|
static TTSMODEINFO found {};
|
||||||
std::vector<VoiceInfo> ret;
|
|
||||||
|
|
||||||
while(!pEnum->Next(1, &found, nullptr)) {
|
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();
|
pEnum->Reset();
|
||||||
return ret;
|
}
|
||||||
|
|
||||||
|
const std::vector<VoiceInfo>& GetVoices() override {
|
||||||
|
return voices;
|
||||||
}
|
}
|
||||||
|
|
||||||
HRESULT SelectVoiceImpl(const GUID& guid) {
|
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.
|
// The above comment is also why this isn't a ComPtr.
|
||||||
AudioOutBuffer* pAudioOut { nullptr };
|
AudioOutBuffer* pAudioOut { nullptr };
|
||||||
|
|
||||||
|
std::vector<VoiceInfo> voices{};
|
||||||
};
|
};
|
||||||
|
|
||||||
ISpeechAPI* ISpeechAPI::CreateSapi4() {
|
ISpeechAPI* ISpeechAPI::CreateSapi4() {
|
||||||
|
|
|
@ -7,7 +7,15 @@ struct ISpeechAPI {
|
||||||
|
|
||||||
struct VoiceInfo {
|
struct VoiceInfo {
|
||||||
GUID guid{}; // Optional. May not be filled out if th e
|
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;
|
virtual ~ISpeechAPI() = default;
|
||||||
|
@ -19,7 +27,7 @@ struct ISpeechAPI {
|
||||||
/// Performs the bare level of initalization required to use the speech API
|
/// Performs the bare level of initalization required to use the speech API
|
||||||
virtual HRESULT Initialize() = 0;
|
virtual HRESULT Initialize() = 0;
|
||||||
|
|
||||||
virtual std::vector<VoiceInfo> GetVoices() = 0;
|
virtual const std::vector<VoiceInfo>& GetVoices() = 0;
|
||||||
|
|
||||||
/// Selects a voice.
|
/// Selects a voice.
|
||||||
virtual HRESULT SelectVoice(std::string_view voiceName) = 0;
|
virtual HRESULT SelectVoice(std::string_view voiceName) = 0;
|
||||||
|
|
Loading…
Reference in a new issue