Compare commits

...

5 commits

Author SHA1 Message Date
Lily Tsuru 126566c3cf voiceinfo list work
(crashes for some reason)
2024-07-18 06:30:57 -04:00
Lily Tsuru 3b127a0b08 remove vs(non dotnet sdk) csproj
not needed, since dotnet sdk tooling can target .net 4.0 (incl windows x86 to boot! pretty cool.).

afaik the project can still be opened by newer vs versions anyways so /shrug
2024-07-18 05:06:37 -04:00
Lily Tsuru 13ce046d3f actually use bindings
about time
2024-07-18 05:05:35 -04:00
Lily Tsuru 1ebd3285f9 remove obsolete source files from speech2 2024-07-18 03:56:02 -04:00
Lily Tsuru bd048875c8 make speech2 a dll + start binding it 2024-07-18 03:54:12 -04:00
26 changed files with 399 additions and 927 deletions

View file

@ -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 ..;

View file

@ -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}");
}
});
}
}
}
}

View file

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

View file

@ -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" />

View file

@ -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
View 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);
}
}
}

View file

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

View file

@ -1,7 +1,5 @@
namespace SAPIServer
{
class VoicesResponse
{
public string[] voices { get; set; }
}
namespace SAPIServer {
class VoicesResponse {
public string[] voices { get; set; }
}
}

View file

@ -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)/:

View file

@ -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

View file

@ -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 $@

View file

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

View file

@ -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

View file

@ -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

View file

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

View file

@ -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

View file

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

View file

@ -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

View file

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

View file

@ -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

View file

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

View file

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

View file

@ -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() {

View file

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