Files
MicroslopSam/host/sapi4host.c
T

344 lines
11 KiB
C

/*
* sapi4host.c — minimal SAPI4 text-to-speech host.
*
* Builds as:
* - Winelib (native Linux ELF): winegcc -m32 -o sapi4host sapi4host.c ...
* - PE (runs under wine): i686-w64-mingw32-gcc -o sapi4host.exe ...
*
* Usage:
* sapi4host -l
* sapi4host -v <voice> -t <text> -o <out.wav> [-p <pitch>] [-s <speed>]
*
* Flow (see reference/speech.h):
* CLSID_TTSEnumerator -> ITTSFind::Find/Select
* CLSID_AudioDestFile -> IAudioFile::Set (WAV)
* ITTSAttributes::PitchSet/SpeedSet
* ITTSNotifySink -> wait for AudioStop
* ITTSCentral::AudioReset / TextData
*
* Derived from the documented SAPI4 flow used by GaryOderNichts/SAPI4-CLI
* and TETYYS/SAPI4 (both small, permissive examples).
*/
#define WIN32_LEAN_AND_MEAN
#include <initguid.h>
#include <windows.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <mmsystem.h>
#include <objbase.h>
#include <speech.h>
static volatile LONG g_done = 0;
static volatile LONG g_started = 0;
/* ------------------------------------------------------------------ */
/* ITTSNotifySink */
/* ------------------------------------------------------------------ */
static HRESULT STDMETHODCALLTYPE N_QueryInterface(ITTSNotifySinkA *This,
REFIID riid, LPVOID *ppv)
{
*ppv = NULL;
if (IsEqualIID(riid, &IID_IUnknown) ||
IsEqualIID(riid, &IID_ITTSNotifySink)) {
*ppv = (LPVOID)This;
return S_OK;
}
return E_NOINTERFACE;
}
static ULONG STDMETHODCALLTYPE N_AddRef(ITTSNotifySinkA *This)
{
(void)This;
return 1;
}
static ULONG STDMETHODCALLTYPE N_Release(ITTSNotifySinkA *This)
{
(void)This;
return 1;
}
static HRESULT STDMETHODCALLTYPE N_AttribChanged(ITTSNotifySinkA *This, DWORD id)
{
(void)This; (void)id;
return S_OK;
}
static HRESULT STDMETHODCALLTYPE N_AudioStart(ITTSNotifySinkA *This, QWORD ts)
{
(void)This; (void)ts;
InterlockedExchange(&g_started, 1);
return S_OK;
}
static HRESULT STDMETHODCALLTYPE N_AudioStop(ITTSNotifySinkA *This, QWORD ts)
{
(void)This; (void)ts;
InterlockedExchange(&g_done, 1);
return S_OK;
}
static HRESULT STDMETHODCALLTYPE N_Visual(ITTSNotifySinkA *This, QWORD ts,
CHAR ipa, CHAR engine,
DWORD hints, PTTSMOUTH mouth)
{
(void)This; (void)ts; (void)hints; (void)mouth;
if (ipa)
fprintf(stderr, "phoneme ipa=0x%02x engine=0x%02x\n",
(unsigned char)ipa, (unsigned char)engine);
return S_OK;
}
static ITTSNotifySinkAVtbl g_notifyVtbl = {
N_QueryInterface,
N_AddRef,
N_Release,
N_AttribChanged,
N_AudioStart,
N_AudioStop,
N_Visual
};
static ITTSNotifySinkA g_notify = { &g_notifyVtbl };
/* ------------------------------------------------------------------ */
/* helpers */
/* ------------------------------------------------------------------ */
static void list_voices(void)
{
PITTSENUM e = NULL;
TTSMODEINFO info;
HRESULT hr = CoCreateInstance(&CLSID_TTSEnumerator, NULL, CLSCTX_ALL,
&IID_ITTSEnum, (void **)&e);
if (FAILED(hr) || !e) {
fprintf(stderr, "cannot create TTS enumerator (0x%08lx)\n",
(unsigned long)hr);
return;
}
for (;;) {
memset(&info, 0, sizeof(info));
if (e->lpVtbl->Next(e, 1, &info, NULL) != S_OK)
break;
printf("%s\n", info.szModeName);
}
e->lpVtbl->Release(e);
}
static HRESULT find_voice(const char *name, TTSMODEINFO *out)
{
PITTSFIND f = NULL;
TTSMODEINFO want, got;
HRESULT hr;
hr = CoCreateInstance(&CLSID_TTSEnumerator, NULL, CLSCTX_ALL,
&IID_ITTSFind, (void **)&f);
if (FAILED(hr) || !f)
return hr;
memset(&want, 0, sizeof(want));
strncpy(want.szModeName, name, sizeof(want.szModeName) - 1);
memset(&got, 0, sizeof(got));
hr = f->lpVtbl->Find(f, &want, NULL, &got);
f->lpVtbl->Release(f);
if (FAILED(hr))
return hr;
*out = got;
return S_OK;
}
static void usage(void)
{
fprintf(stderr,
"usage: sapi4host -l\n"
" sapi4host -v <voice> -t <text> -o <out.wav>"
" [-p pitch] [-s speed]\n");
}
/* ------------------------------------------------------------------ */
/* main */
/* ------------------------------------------------------------------ */
int main(int argc, char **argv)
{
const char *voice = NULL, *text = NULL, *out = NULL;
int pitch = -1, speed = -1;
int i;
TTSMODEINFO mode;
for (i = 1; i < argc; i++) {
if (!strcmp(argv[i], "-l") || !strcmp(argv[i], "--list")) {
CoInitialize(NULL);
list_voices();
CoUninitialize();
return 0;
} else if (!strcmp(argv[i], "-v")) voice = argv[++i];
else if (!strcmp(argv[i], "-t")) text = argv[++i];
else if (!strcmp(argv[i], "-o")) out = argv[++i];
else if (!strcmp(argv[i], "-p")) pitch = atoi(argv[++i]);
else if (!strcmp(argv[i], "-s")) speed = atoi(argv[++i]);
else { usage(); return 2; }
}
if (!voice || !text || !out) { usage(); return 2; }
if (FAILED(CoInitialize(NULL))) {
fprintf(stderr, "CoInitialize failed\n");
return 1;
}
if (FAILED(find_voice(voice, &mode))) {
fprintf(stderr, "voice not found: %s\navailable:\n", voice);
list_voices();
CoUninitialize();
return 1;
}
printf("voice: %s\n", mode.szModeName);
PIAUDIOFILE af = NULL;
HRESULT hr = CoCreateInstance(&CLSID_AudioDestFile, NULL, CLSCTX_ALL,
&IID_IAudioFile, (void **)&af);
if (FAILED(hr) || !af) {
fprintf(stderr, "cannot create audio destination (0x%08lx)\n",
(unsigned long)hr);
CoUninitialize();
return 1;
}
PITTSCENTRAL central = NULL;
{
PITTSFIND f = NULL;
hr = CoCreateInstance(&CLSID_TTSEnumerator, NULL, CLSCTX_ALL,
&IID_ITTSFind, (void **)&f);
if (FAILED(hr) || !f) {
fprintf(stderr, "cannot create TTS find (0x%08lx)\n",
(unsigned long)hr);
af->lpVtbl->Release(af);
CoUninitialize();
return 1;
}
hr = f->lpVtbl->Select(f, mode.gModeID, &central, (LPUNKNOWN)af);
f->lpVtbl->Release(f);
if (FAILED(hr) || !central) {
fprintf(stderr, "voice select failed (0x%08lx)\n",
(unsigned long)hr);
af->lpVtbl->Release(af);
CoUninitialize();
return 1;
}
}
af->lpVtbl->RealTimeSet(af, (WORD)TTSATTR_MAXREALTIME);
WCHAR wout[MAX_PATH];
MultiByteToWideChar(CP_ACP, 0, out, -1, wout, MAX_PATH);
hr = af->lpVtbl->Set(af, wout, 1);
if (FAILED(hr)) {
fprintf(stderr, "cannot open output file (0x%08lx)\n",
(unsigned long)hr);
central->lpVtbl->Release(central);
af->lpVtbl->Release(af);
CoUninitialize();
return 1;
}
PITTSATTRIBUTES attr = NULL;
hr = central->lpVtbl->QueryInterface(central, &IID_ITTSAttributes,
(void **)&attr);
if (FAILED(hr) || !attr) {
fprintf(stderr, "no ITTSAttributes (0x%08lx)\n", (unsigned long)hr);
central->lpVtbl->Release(central);
af->lpVtbl->Release(af);
CoUninitialize();
return 1;
}
/* Pitch and speed are optional: when unset, leave the engine's own
voice profile untouched (probing/setting would override it). */
if (pitch >= 0 || speed >= 0) {
WORD pmin = 0, pmax = 0;
DWORD smin = 0, smax = 0;
if (pitch >= 0) {
attr->lpVtbl->PitchSet(attr, TTSATTR_MINPITCH);
attr->lpVtbl->PitchGet(attr, &pmin);
attr->lpVtbl->PitchSet(attr, TTSATTR_MAXPITCH);
attr->lpVtbl->PitchGet(attr, &pmax);
if (pitch < (int)pmin) pitch = pmin;
if (pitch > (int)pmax) pitch = pmax;
attr->lpVtbl->PitchSet(attr, (WORD)pitch);
printf("pitch: %d (%d..%d)\n", pitch, pmin, pmax);
}
if (speed >= 0) {
attr->lpVtbl->SpeedSet(attr, TTSATTR_MINSPEED);
attr->lpVtbl->SpeedGet(attr, &smin);
attr->lpVtbl->SpeedSet(attr, TTSATTR_MAXSPEED);
attr->lpVtbl->SpeedGet(attr, &smax);
if (speed < (int)smin) speed = smin;
if (speed > (int)smax) speed = smax;
attr->lpVtbl->SpeedSet(attr, (DWORD)speed);
printf("speed: %d (%lu..%lu)\n",
speed, (unsigned long)smin, (unsigned long)smax);
}
}
{
DWORD regkey = 0;
hr = central->lpVtbl->Register(central, &g_notify,
IID_ITTSNotifySink, &regkey);
if (FAILED(hr)) {
fprintf(stderr, "Register failed (0x%08lx)\n", (unsigned long)hr);
attr->lpVtbl->Release(attr);
central->lpVtbl->Release(central);
af->lpVtbl->Release(af);
CoUninitialize();
return 1;
}
}
{
SDATA data;
data.dwSize = (DWORD)(strlen(text) + 1);
data.pData = (PVOID)text;
central->lpVtbl->AudioReset(central);
hr = central->lpVtbl->TextData(central, CHARSET_TEXT,
TTSDATAFLAG_TAGGED, data, NULL,
IID_ITTSBufNotifySink);
if (FAILED(hr)) {
fprintf(stderr, "TextData failed (0x%08lx)\n", (unsigned long)hr);
} else {
DWORD start = GetTickCount();
while (!g_done) {
MSG msg;
while (PeekMessage(&msg, NULL, 0, 0, PM_REMOVE)) {
TranslateMessage(&msg);
DispatchMessage(&msg);
}
if (!g_started && GetTickCount() - start > 30000) {
fprintf(stderr, "timed out waiting for audio start\n");
break;
}
if (GetTickCount() - start > 120000) {
fprintf(stderr, "timed out waiting for audio stop\n");
break;
}
Sleep(10);
}
}
}
af->lpVtbl->Flush(af);
attr->lpVtbl->Release(attr);
central->lpVtbl->Release(central);
af->lpVtbl->Release(af);
CoUninitialize();
printf("done%s\n", g_done ? "" : " (incomplete)");
return g_done ? 0 : 1;
}