/* * 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 -t -o [-p ] [-s ] * * 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 #include #include #include #include #include #include #include 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 -t -o " " [-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; }