Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles
(untagged)

Creating Microsoft SAPI Compliant Application(s)

0.00/5 (No votes)
24 Feb 2004 5  
This article will explain how to create a Microsoft SAPI compliant Application or Integrating a TTS with SAPI.

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()

// ISAPIObj

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; 
        }

    // ISpTTSEngine 

    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

//////////////////////////////////////////////////////////////////

// DLLRegisterServer - Adds entries to the system registry


STDAPI DLLRegisterServer(void)
{
#ifdef _MERGE_PROXYSTUB
    HRESULT hRes = PrxDLLRegisterServer();
    if (FAILED(hRes))
        return hRes;
#endif
    // registers object, typelib and all interfaces in typelib

    return _Module.RegisterServer(TRUE);
}

//////////////////////////////////////////////////////////////////

// DLLUnregisterServer - Removes entries from the system registry


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

////////////////////////////////////////////////////////////////

// DLLRegisterServer - Adds entries to the system registry


STDAPI DLLRegisterServer(void)
{
#ifdef _MERGE_PROXYSTUB
    HRESULT hRes = PrxDLLRegisterServer();
    if (FAILED(hRes))
        return hRes;
#endif
        //      Make Entry in the Registry for The Voice

        HKEY    lKey;
        DWORD   LocalDisp = 0;
        //      Try to Open 

        
        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;
        }
        //      Close the Key 

        RegCloseKey ( lKey );
        LocalDisp = 0;
        //      Now Open Attributes

        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;
        }
        //      Close the Key

        RegCloseKey ( lKey );
    // registers object, typelib and all interfaces in typelib

    return _Module.RegisterServer(TRUE);
}

////////////////////////////////////////////////////////////////////

// DLLUnregisterServer - Removes entries from the system registry


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.

License

This article has no explicit license attached to it but may contain usage terms in the article text or the download files themselves. If in doubt please contact the author via the discussion board below.

A list of licenses authors might use can be found here