353 lines
11 KiB
C
353 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] [-V volume]\n");
|
|
}
|
|
|
|
/* ------------------------------------------------------------------ */
|
|
/* main */
|
|
/* ------------------------------------------------------------------ */
|
|
|
|
int main(int argc, char **argv)
|
|
{
|
|
const char *voice = NULL, *text = NULL, *out = NULL;
|
|
int pitch = -1, speed = -1;
|
|
long volume = -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 if (!strcmp(argv[i], "-V")) volume = strtol(argv[++i], NULL, 0);
|
|
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, ¢ral, (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, speed and volume are optional: when unset, leave the engine's own
|
|
voice profile untouched (probing/setting would override it). */
|
|
if (pitch >= 0 || speed >= 0 || volume >= 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);
|
|
}
|
|
if (volume >= 0) {
|
|
DWORD vdef = 0;
|
|
attr->lpVtbl->VolumeGet(attr, &vdef);
|
|
attr->lpVtbl->VolumeSet(attr, (DWORD)volume);
|
|
printf("volume: %ld (default %lu)\n",
|
|
volume, (unsigned long)vdef);
|
|
}
|
|
}
|
|
|
|
{
|
|
DWORD regkey = 0;
|
|
hr = central->lpVtbl->Register(central, &g_notify,
|
|
IID_ITTSNotifySink, ®key);
|
|
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;
|
|
}
|