Introduction and Motivations
This article is to explain and demonstrate what is meant by a SAPI compliant Application. This example also illustrates the way to minimally integrate a TTS with the SAPI
Background
If you want to plug a TTS into Microsoft's SAPI then you can do it easily by creating a SAPI Compliant Application Framework using ActiveX/COM. Any application compliance with SAPI means any application that is using the SAPI should be able to call your application using the same piece of code that is used for the SAPI, by merely changing the parameters of some of the functions.
Here I'm showing you a minimal SAPI compliant application that can be used for Integrating you application with SAPI. This is done by overriding ( in C++ terms ) the member functions of the SAPI Interface from which your custom interfaces have been derived.
Using the code
Here I will guide you for creating a SAPI compliant application. First of all you have to create a ActiveX component using ATL COM AppWizard of the MSVC++ 6.0 ( you are free to use .NET version, but I haven't tested this on that ).
To create an ActiveX component under VC++ 6.0, go through following steps
1. Create an ActiveX DLL using ATL COM AppWizard.
2. Add a ATL Object in the component and named it as SAPIObj.
3. The Wizard will generate number of files including .idl, .c, .rc, and .cpp files
Changing the generated IDL file
The IDL code that is generated by the ATL COM AppWizard will look like the code given below. of course the UUID will be different in your case
import "oaidl.idl";
import "ocidl.idl";
[
object,
uuid(29B9CEB1-5C49-11D8-8333-5254AB2226C5),
dual,
helpstring("ISAPIObj Interface"),
pointer_default(unique)
]
interface ISAPIObj : IDispatch
{
};
[
uuid(779D02D1-5AC3-11D8-832D-5254AB2226C5),
version(1.0),
helpstring("SAPIComp 1.0 Type Library")
]
library SAPICOMPLib
{
importlib("stdole32.tlb");
importlib("stdole2.tlb");
[
uuid(779D02E0-5AC3-11D8-832D-5254AB2226C5),
helpstring("SAPIObj Class")
]
coclass SAPIObj
{
[default] interface ISAPIObj;
};
};
Since we have to implement the SAPI interface, so we need not to have our own interface defined in the IDL file, so what we can do is that we can remove our custom interface and our class factory (coclass) and will have SAPI interface as default instead of custom interface. we can take the SAPI interface definition by importing the "sapiddk.idl" file which comes along with sapi. For a minimal SAPI compliant application in which we can overload the speak function we need to have two SAPI interface they are ISpTTSEngine
and ISpObjectWithToken
. The new code will look like
import "oaidl.idl";
import "ocidl.idl";
import "sapiddk.idl";
[
uuid(779D02D1-5AC3-11D8-832D-5254AB2226C5),
version(1.0),
helpstring("SAPIComp 1.0 Type Library")
]
library SAPICOMPLib
{
importlib("stdole32.tlb");
importlib("stdole2.tlb");
[
uuid(779D02E0-5AC3-11D8-832D-5254AB2226C5),
helpstring("SAPIObj Class")
]
coclass SAPIObj
{
[default] interface ISpTTSEngine;
interface ISpObjectWithToken;
};
};
Changing the generated Class Header file
Now its the time to write our own implementation. since we have removed our custom interface that is being generated by the ATL COM AppWizard we must also remove any reference of that particular interface in public derivations of your class as well as from the BEGIN_COM_MAP
and END_COM_MAP
.
The generated SAPIObj.h header file will look like
class ATL_NO_VTABLE CSAPIObj :
public CComObjectRootEx<CComSingleThreadModel>,
public CComCoClass<CSAPIObj, &CLSID_SAPIObj>,
public IDispatchImpl<ISAPIObj, &IID_ISAPIObj, &LIBID_ABCLib>
{
public:
CSAPIObj()
{
}
DECLARE_REGISTRY_RESOURCEID(IDR_SAPIOBJ)
DECLARE_PROTECT_FINAL_CONSTRUCT()
BEGIN_COM_MAP(CSAPIObj)
COM_INTERFACE_ENTRY(ISAPIObj)
COM_INTERFACE_ENTRY(IDispatch)
END_COM_MAP()
public:
};
since we are not having out custom interface we must remove the line public IDispatchImpl<ISAPIObj, &IID_ISAPIObj, &LIBID_ABCLib>
and COM_INTERFACE_ENTRY
of ISAPIObj
and IDispatch
and add the line public ISpTTSEngine, public ISpObjectWithToken
in the derivation list and ISpTTSEngine
and ISpObjectWithToken
in the COM_INTERFACE_ENTRY
The changed code will look like
class ATL_NO_VTABLE CSAPIObj :
public CComObjectRootEx<CComSingleThreadModel>,
public CComCoClass<CSAPIObj, &CLSID_SAPIObj>,
public ISpTTSEngine,
public ISpObjectWithToken
{
public:
CSAPIObj()
{
}
DECLARE_REGISTRY_RESOURCEID(IDR_SAPIOBJ)
DECLARE_PROTECT_FINAL_CONSTRUCT()
BEGIN_COM_MAP(CSAPIObj)
COM_INTERFACE_ENTRY(ISpTTSEngine)
COM_INTERFACE_ENTRY(ISpObjectWithToken)
END_COM_MAP()
public:
};
Now the real thing we have to implement the method that must be called when SAPI will select our Application or TTS. so we must write definition of the functions of the interface ISpTTSEngine
and ISpObjectWithToken
. The header file along with function definitions will look like
class ATL_NO_VTABLE CSAPIObj :
public CComObjectRootEx<CComSingleThreadModel>,
public CComCoClass<CSAPIObj, &CLSID_SAPIObj>,
public ISpTTSEngine,
public ISpObjectWithToken
{
public:
CSAPIObj()
{
}
DECLARE_REGISTRY_RESOURCEID(IDR_SAPIOBJ)
DECLARE_PROTECT_FINAL_CONSTRUCT()
BEGIN_COM_MAP(CSAPIObj)
COM_INTERFACE_ENTRY(ISpTTSEngine)
COM_INTERFACE_ENTRY(ISpObjectWithToken)
END_COM_MAP()
STDMETHODIMP SetObjectToken( ISpObjectToken * pToken )
{
return S_OK;
}
STDMETHODIMP GetObjectToken( ISpObjectToken ** ppToken )
{
return S_OK;
}
STDMETHOD(Speak)( DWORD dwSpeakFlags,
REFGUID rguidFormatId,
const WAVEFORMATEX * pWaveFormatEx,
const SPVTEXTFRAG* pTextFragList,
ISpTTSEngineSite* pOutputSite )
{
MessageBox ( 0 , "This Is me......." , "Msg" , 0 );
return S_OK;
}
STDMETHOD(GetOutputFormat)( const GUID * pTargetFormatId,
const WAVEFORMATEX * pTargetWaveFormatEx,
GUID * pDesiredFormatId,
WAVEFORMATEX ** ppCoMemDesiredWaveFormatEx )
{
return S_OK;
}
public:
};
Now your are done. you have just created an application that overloads the SAPI speak function and it will call this speak function just containing a message box when applications like SAPI sample applications TTSApp is used.
okay I have just said that everything is done, is it so? To some extent its yes and to some extent its no. yes because you have created an application that is compatible and no because SAPI Framework didn't know anything about your application so how it is going to call this application.
Generating SAPI registry Entry
The answer to the no is present in the registry. SAPI has a specific path in the registry containing the voices and corresponding application CLSID(s). The path to the registry is HKEY_LOCAL_MACHINE\\Software\\Microsoft\\Speech\\Voices\\Tokens
. if SAPI is installed on your system and you look at this particular registry key you will be able to look at various speech(es) that is there in the system. To let SAPI know about your application you must have a entry in voices with the CLSID of the Application we have just created. This enables SAPI to list Application Specific Voices and also call Application specific overriding functions when SAPI framework select your voice.
Making an entry to the registry is simple as every ActiveX/COM objects registered itself to the registry and call the DLLRegisterServer
and DLLUnregisterServer
when the ActiveX/COM component is unregistered. These function is also generated by the ATL COM AppWizard and present in the .cpp file generated by the wizard. The Original generated function will look like
STDAPI DLLRegisterServer(void)
{
#ifdef _MERGE_PROXYSTUB
HRESULT hRes = PrxDLLRegisterServer();
if (FAILED(hRes))
return hRes;
#endif
return _Module.RegisterServer(TRUE);
}
STDAPI DLLUnregisterServer(void)
{
#ifdef _MERGE_PROXYSTUB
PrxDLLUnregisterServer();
#endif
return _Module.UnregisterServer(TRUE);
}
We can take advantage of these two functions as we will do so. we will make an entry in the registry about the custom voice as soon as the component registered itself and remove the entry from the registry as soon as the Component unregistered itself. The changed code will look like
STDAPI DLLRegisterServer(void)
{
#ifdef _MERGE_PROXYSTUB
HRESULT hRes = PrxDLLRegisterServer();
if (FAILED(hRes))
return hRes;
#endif
HKEY lKey;
DWORD LocalDisp = 0;
long lResult = RegCreateKeyEx ( HKEY_LOCAL_MACHINE,
"Software\\Microsoft\\Speech\\Voices\\Tokens\\MyTTSOption", 0,
"MyTTSOption" , REG_OPTION_NON_VOLATILE ,
KEY_ALL_ACCESS ,0 ,&lKey , &LocalDisp );
if ( lResult != ERROR_SUCCESS )
{
MessageBox ( 0," Error Installing Custom SAPI" ,
"Error Message.." , 0 );
return E_FAIL;
}
char LocalData [ 256 ];
memset ( LocalData , 0 , 256 );
strcpy ( LocalData , "MySampleTTS" );
lResult = RegSetValueEx ( lKey , "409", NULL ,REG_SZ,
( LPBYTE )LocalData , strlen ( LocalData ) );
if ( lResult != ERROR_SUCCESS )
{
MessageBox ( 0," Error Installing SAPI Application. "
"Either SAPI is not installed on your system"
" or you do not have"
" suffecient rights to do so. Please contact your "
"system Administrator " , "Error Message.." , 0 );
return E_FAIL;
}
memset ( LocalData , 0 , 256 );
strcpy ( LocalData , "{779D02E0-5AC3-11D8-832D-5254AB2226C5}" );
lResult = RegSetValueEx ( lKey , "CLSID", NULL ,
REG_SZ, ( LPBYTE )LocalData , strlen ( LocalData ) );
if ( lResult != ERROR_SUCCESS )
{
MessageBox ( 0," Error Installing Custom SAPI" ,
"Error Message.." , 0 );
return E_FAIL;
}
memset ( LocalData , 0 , 256 );
strcpy ( LocalData , "MySample_Voice_Data" );
lResult = RegSetValueEx ( lKey , "VoiceData", NULL ,REG_SZ,
( LPBYTE )LocalData , strlen ( LocalData ) );
if ( lResult != ERROR_SUCCESS )
{
MessageBox ( 0," Error Installing SAPI Application."
" Either SAPI is not installed on your system or you"
" do not have suffecient rights to do so. Please contact"
" your system Administrator " , "Error Message.." , 0 );
return E_FAIL;
}
RegCloseKey ( lKey );
LocalDisp = 0;
lResult = RegCreateKeyEx ( HKEY_LOCAL_MACHINE,
"Software\\Microsoft\\Speech\\Voices\\Tokens\\MyTTSOption\\Attributes",
0, "Attributes" , REG_OPTION_NON_VOLATILE ,KEY_ALL_ACCESS ,
0 ,&lKey , &LocalDisp );
if ( lResult != ERROR_SUCCESS )
{
MessageBox ( 0," Error Installing SAPI Application. "
"Either SAPI is not installed on your"
" system or you do not have "
"suffecient rights to do so. Please contact your system"
" Administrator " , "Error Message.." , 0 );
return E_FAIL;
}
memset ( LocalData , 0 , 256 );
strcpy ( LocalData , "Adult" );
lResult = RegSetValueEx ( lKey , "Age", NULL ,REG_SZ,
( LPBYTE )LocalData , strlen ( LocalData ) );
if ( lResult != ERROR_SUCCESS )
{
MessageBox ( 0," Error Installing SAPI Application. "
"Either SAPI is not installed on your system or you do not"
" have suffecient rights to do so. Please contact your"
" system Administrator " , "Error Message.." , 0 );
return E_FAIL;
}
memset ( LocalData , 0 , 256 );
strcpy ( LocalData , "Male" );
lResult = RegSetValueEx ( lKey , "Gender",
NULL ,REG_SZ, ( LPBYTE )LocalData , strlen ( LocalData ) );
if ( lResult != ERROR_SUCCESS )
{
MessageBox ( 0," Error Installing SAPI Application."
" Either SAPI is not installed on your system or you do not "
"have suffecient rights to do so. Please contact your "
"system Administrator " , "Error Message.." , 0 );
return E_FAIL;
}
memset ( LocalData , 0 , 256 );
strcpy ( LocalData , "409;9" );
lResult = RegSetValueEx ( lKey , "Language", NULL ,REG_SZ,
( LPBYTE )LocalData , strlen ( LocalData ) );
if ( lResult != ERROR_SUCCESS )
{
MessageBox ( 0," Error Installing SAPI Application. "
"Either SAPI is not installed on your system or you do not"
" have suffecient rights to do so. Please contact your"
" system Administrator " , "Error Message.." , 0 );
return E_FAIL;
}
memset ( LocalData , 0 , 256 );
strcpy ( LocalData , "MyTTSOption" );
lResult = RegSetValueEx ( lKey , "Name", NULL ,REG_SZ,
( LPBYTE )LocalData , strlen ( LocalData ) );
if ( lResult != ERROR_SUCCESS )
{
MessageBox ( 0,
" Error Installing SAPI Application. Either SAPI is"
" not installed on your system or"
" you do not have suffecient rights"
" to do so. Please contact your system Administrator " ,
"Error Message.." , 0 );
return E_FAIL;
}
memset ( LocalData , 0 , 256 );
strcpy ( LocalData , "General...." );
lResult = RegSetValueEx ( lKey , "Vendor", NULL ,REG_SZ,
( LPBYTE )LocalData , strlen ( LocalData ) );
if ( lResult != ERROR_SUCCESS )
{
MessageBox ( 0," Error Installing SAPI Application."
" Either SAPI is not installed on your system or you do "
"not have suffecient rights to do so. Please contact your "
"system Administrator " , "Error Message.." , 0 );
return E_FAIL;
}
RegCloseKey ( lKey );
return _Module.RegisterServer(TRUE);
}
STDAPI DLLUnregisterServer(void)
{
HKEY lKey;
long lResult = RegOpenKeyEx ( HKEY_LOCAL_MACHINE ,
"Software\\Microsoft\\Speech\\Voices\\Tokens\\MyTTSOption",
0 , KEY_ALL_ACCESS , &lKey );
if ( lResult != ERROR_SUCCESS )
MessageBox ( 0 ,"Unable to DE Install Application properly, "
"you may have to remove some entries manually ",
"Error DeInstall" , 0 );
lResult = RegDeleteKey ( lKey , "Attributes" );
if ( lResult != ERROR_SUCCESS )
MessageBox ( 0 ,"Unable to DE Install Application properly,"
" you may have to remove some entries manually ",
"Error DeInstall" , 0 );
lResult = RegCloseKey ( lKey );
lResult = RegOpenKey ( HKEY_LOCAL_MACHINE ,
"Software\\Microsoft\\Speech\\Voices\\Tokens", &lKey );
if ( lResult != ERROR_SUCCESS )
MessageBox ( 0 ,"Unable to DE Install Application "
"properly, you may have to remove some entries manually ",
"Error DeInstall" , 0 );
lResult = RegDeleteKey ( lKey , "MyTTSOption" );
if ( lResult != ERROR_SUCCESS )
MessageBox ( 0 ,"Unable to DE Install Application properly,"
" you may have to remove some entries manually ",
"Error DeInstall" , 0 );
lResult = RegCloseKey ( lKey );
#ifdef _MERGE_PROXYSTUB
PrxDLLUnregisterServer();
#endif
return _Module.UnregisterServer(TRUE);
}
Now you are completely done. you SAPI compliant application is ready. compile and register this component and test the application you can test this application by using TTSApp sample application provided by the SAPI
Points of Interest
Initially I was trying very hard to come up with the SAPI compliant application and I was even successful in doing so by using MFC based applications, but this was really simple and will help people to understand not only SAPI but also some of its framework.
History
- This is the latest version.