Winelib SAPI4 host: MSTTS (Sam/Mary/Mike) + AT&T Flextalk (Keith Bell/Kerry Watson)
This commit is contained in:
@@ -0,0 +1,339 @@
|
||||
/*
|
||||
* 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, ¢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;
|
||||
}
|
||||
|
||||
WORD pmin = 0, pmax = 0, pdef = 0;
|
||||
DWORD smin = 0, smax = 0, sdef = 0;
|
||||
attr->lpVtbl->PitchGet(attr, &pdef);
|
||||
attr->lpVtbl->SpeedGet(attr, &sdef);
|
||||
attr->lpVtbl->PitchSet(attr, TTSATTR_MINPITCH);
|
||||
attr->lpVtbl->PitchGet(attr, &pmin);
|
||||
attr->lpVtbl->PitchSet(attr, TTSATTR_MAXPITCH);
|
||||
attr->lpVtbl->PitchGet(attr, &pmax);
|
||||
attr->lpVtbl->SpeedSet(attr, TTSATTR_MINSPEED);
|
||||
attr->lpVtbl->SpeedGet(attr, &smin);
|
||||
attr->lpVtbl->SpeedSet(attr, TTSATTR_MAXSPEED);
|
||||
attr->lpVtbl->SpeedGet(attr, &smax);
|
||||
|
||||
if (pitch < 0) pitch = pdef;
|
||||
if (speed < 0) speed = sdef;
|
||||
if (pitch < (int)pmin) pitch = pmin;
|
||||
if (pitch > (int)pmax) pitch = pmax;
|
||||
if (speed < (int)smin) speed = smin;
|
||||
if (speed > (int)smax) speed = smax;
|
||||
printf("pitch: %d (%d..%d) speed: %d (%lu..%lu)\n",
|
||||
pitch, pmin, pmax, speed, (unsigned long)smin, (unsigned long)smax);
|
||||
|
||||
attr->lpVtbl->PitchSet(attr, (WORD)pitch);
|
||||
attr->lpVtbl->SpeedSet(attr, (DWORD)speed);
|
||||
|
||||
{
|
||||
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;
|
||||
}
|
||||
Reference in New Issue
Block a user