MS Speech SAPI Text to speech COM (Windows only)

Share your advanced PureBasic knowledge/code with the community.
jassing
Addict
Addict
Posts: 1885
Joined: Wed Feb 17, 2010 12:00 am

Re: MS Speech SAPI Text to speech COM (Windows only)

Post by jassing »

Justin wrote: Wed May 10, 2023 9:19 pm Those voices are 32 bit only, run the example with the 32 bit version of Purebasic on your 64 bit machine and it should work. You will need to compile a 32 bit exe if you want to distribute your program.
Weird, I ran the 64bit installer.
Justin
Addict
Addict
Posts: 948
Joined: Sat Apr 26, 2003 2:49 pm

Re: MS Speech SAPI Text to speech COM (Windows only)

Post by Justin »

I downloaed a demo voice from here
https://www.cepstral.com/en/personal/download
And there isn't a 32/64 bit option and it's installed in the x86 Programs folder Anyways is working now.
jassing
Addict
Addict
Posts: 1885
Joined: Wed Feb 17, 2010 12:00 am

Re: MS Speech SAPI Text to speech COM (Windows only)

Post by jassing »

Justin wrote: Thu May 11, 2023 9:02 am I downloaed a demo voice from here
https://www.cepstral.com/en/personal/download
And there isn't a 32/64 bit option and it's installed in the x86 Programs folder Anyways is working now.
Open the exe with something like 7z, inside it, there are two MSI's, one 32 and one 64
Image
marcos.exe
User
User
Posts: 21
Joined: Fri Jan 17, 2020 8:20 pm

Re: MS Speech SAPI Text to speech COM (Windows only)

Post by marcos.exe »

Hello everybody!
The voice I'm using doesn't appear in my screen reader:
scansoft Raquel brazilian portuguese 22khz

I know it's called that because that's what's on my screen reader.

It doesn't appear in the list, and I don't know how to select it in the code either.
32 and 64 bit. It's nothing...
Any idea?

Thanks in advance!
When our generation/OS updates, we either update ourselves, or we are removed.
But we are never fully uninstalled.
jassing
Addict
Addict
Posts: 1885
Joined: Wed Feb 17, 2010 12:00 am

Re: MS Speech SAPI Text to speech COM (Windows only)

Post by jassing »

Because it's a nextup voice, not one from MS?
marcos.exe
User
User
Posts: 21
Joined: Fri Jan 17, 2020 8:20 pm

Re: MS Speech SAPI Text to speech COM (Windows only)

Post by marcos.exe »

It is a common sapi 5 voice. Recognized by programs like balabouca and the PureTts library (available only for PureBasic v4.00), among other programs and screen readers.

https://superusuarios.com/download/voz- ... nstalavel/
Only Portuguese from Brazil.
When our generation/OS updates, we either update ourselves, or we are removed.
But we are never fully uninstalled.
Jens-Arne
User
User
Posts: 43
Joined: Sun Feb 04, 2024 11:09 am

Re: MS Speech SAPI Text to speech COM (Windows only)

Post by Jens-Arne »

Really great work!

I have simplified things further so you don't have to bother about anything but to supply the text to be read out. Just one pbi needed in which everything is included.

These are the only functions you need to know to do all the work:

Code: Select all

sapi_Init(hMainWnd.i,UsermessageNumber.i=#WM_USER) ;call once at the beginning, supply ID (MS-Windows handle) of main program window; optional: set user message number that is used internally by the pbi if #WM_USER is already in use by your program
sapi_DeInit() ;call once at the end
sapi_Say(Text$)
sapi_Pause()
sapi_Resume()
sapi_Stop()
sapi_GetVoiceNames() ;returns the names of all installed voices separated by "|"
sapi_SetVoice(VoiceName$) ;VoiceName$=one of the names returned by sapi_GetVoiceNames()
sapi_SetSpeed(Speed.b=0) ;-10 to +10; default=0
sapi_SetVolume(Volume.b=100) ;0 to 100; default=100
sapi_IsVoiceRunning() ;returns 1 if text is played (even when paused), 0 otherwise
sapi_IsVoicePaused() ;returns 1 if readout is paused, 0 otherwise
sapi_GetLastBookmark() ;returns the name of the last bookmark reached; e.g. »<bookmark mark="bookmark_one"/>« in the text
sapi_GetCurrentWordPos(TakeChr13IntoAccount.i=0) ;returns the position of the word currently read out
sapi_GetCurrentWordLen() ;returns the length of the word currently read out
All functions (but the last two of course) return 1 if successful and usually 0 otherwise. Some return -1 or -2 on errors to indicate details (for example -1 on sapi_Init means that sapi is already initialized, -2 means that the voices couldn't be enumerated but the module was initialized anyway).
An error code of -1 always means that sapi_Init wasn't called at the beginning (exception: sapi_Init).

Text is read out asynchronously (sapi_Say returns immediately after having commenced the readout). sapi_IsVoiceRunning() can be used to determine whether a text has been read out completely.

Please note that subsequent calls to sapi_Say will automatically stop reading out a previously started text that hasn't finished yet and start over with the new text.

The first piece of code should be named "sapi-all.pbi". The second one is an example of how to use it.

Code: Select all

;sapi-all.pbi
;see here: https://www.purebasic.fr/english/viewtopic.php?t=71402

;{ ;Version history
;}
;V1.00: first distribution version [12.01.2025]
;V1.01: voice procedures added [12.01.2025]
;V1.02: own usermessage number added [12.01.2025]
;V1.03: sapi_SetSpeed & sapi_SetVolume added [12.01.2025]
;V1.04: sapi_IsVoiceRunning & sapi_IsVoicePaused added [13.01.2025]
;V1.05: errorhandling for sapi_SetVoice implemented [18.01.2025]
;V1.06: UserMessages for speech start & stop implemented [18.01.2025]
;V1.07: switched to alternative voice source [18.01.2025]
;V1.08: Windows XP voices added (Sam, Mike, Mary) [18.01.2025]
;V1.09: processing of bookmarks added [19.01.2025]
;V1.10: #SPEI_WORD_BOUNDARY event added [19.01.2025]

;{ ;sapi.pbi
#SPCAT_VOICES1 = "HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Speech\Voices"
#SPCAT_VOICES2 = "HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Speech_OneCore\Voices"

;- enum SPCFGRULEATTRIBUTES
#SPRAF_TopLevel	= 1 << 0
#SPRAF_Active	= 1 << 1
#SPRAF_Export	= 1 << 2
#SPRAF_Import	= 1 << 3
#SPRAF_Interpreter	= 1 << 4
#SPRAF_Dynamic	= 1 << 5
#SPRAF_AutoPause	= 1 << 16

;- enum SPGRAMMARWORDTYPE
#SPWT_DISPLAY	= 0
#SPWT_LEXICAL	= #SPWT_DISPLAY + 1
#SPWT_PRONUNCIATION	= #SPWT_LEXICAL + 1

;- enum SPEVENTENUM
#SPEI_UNDEFINED	= 0
#SPEI_START_INPUT_STREAM	= 1
#SPEI_END_INPUT_STREAM	= 2
#SPEI_VOICE_CHANGE	= 3
#SPEI_TTS_BOOKMARK	= 4
#SPEI_WORD_BOUNDARY	= 5
#SPEI_PHONEME	= 6
#SPEI_SENTENCE_BOUNDARY	= 7
#SPEI_VISEME	= 8
#SPEI_TTS_AUDIO_LEVEL	= 9
#SPEI_TTS_PRIVATE	= 15
#SPEI_MIN_TTS	= 1
#SPEI_MAX_TTS	= 15
#SPEI_END_SR_STREAM	= 34
#SPEI_SOUND_START	= 35
#SPEI_SOUND_END	= 36
#SPEI_PHRASE_START	= 37
#SPEI_RECOGNITION	= 38
#SPEI_HYPOTHESIS	= 39
#SPEI_SR_BOOKMARK	= 40
#SPEI_PROPERTY_NUM_CHANGE	= 41
#SPEI_PROPERTY_STRING_CHANGE	= 42
#SPEI_FALSE_RECOGNITION	= 43
#SPEI_INTERFERENCE	= 44
#SPEI_REQUEST_UI	= 45
#SPEI_RECO_STATE_CHANGE	= 46
#SPEI_ADAPTATION	= 47
#SPEI_START_SR_STREAM	= 48
#SPEI_RECO_OTHER_CONTEXT	= 49
#SPEI_SR_AUDIO_LEVEL	= 50
#SPEI_SR_PRIVATE	= 52
#SPEI_MIN_SR	= 34
#SPEI_MAX_SR	= 52
#SPEI_RESERVED1	= 30
#SPEI_RESERVED2	= 33
#SPEI_RESERVED3	= 63

#SPFEI_FLAGCHECK = ((1 << #SPEI_RESERVED1) | (1 << #SPEI_RESERVED2) )
#SPFEI_ALL_TTS_EVENTS = ($000000000000FFFE | #SPFEI_FLAGCHECK) 
#SPFEI_ALL_SR_EVENTS = ($001FFFFC00000000 | #SPFEI_FLAGCHECK)
#SPFEI_ALL_EVENTS = $EFFFFFFFFFFFFFFF
Macro SPFEI(SPEI_ord) : ((1 << SPEI_ord) | #SPFEI_FLAGCHECK) : EndMacro 

;- enum SPRULESTATE
#SPRS_INACTIVE	= 0
#SPRS_ACTIVE	= 1
#SPRS_ACTIVE_WITH_AUTO_PAUSE	= 3

;enum SPRUNSTATE
#SPRS_DONE = 1 << 0
#SPRS_IS_SPEAKING = 1 << 1

;- enum SPPHRASERNG
#SPPR_ALL_ELEMENTS	= -1

#SP_GETWHOLEPHRASE = #SPPR_ALL_ELEMENTS
#SPRR_ALL_ELEMENTS = #SPPR_ALL_ELEMENTS

;- enum SPGRAMMARSTATE
#SPGS_ENABLED = 0
#SPGS_DISABLED = 1
#SPGS_EXCLUSIVE = 3

;- enum SPLOADOPTIONS
#SPLO_STATIC	= 0
#SPLO_DYNAMIC	= 1

;- enum SPEAKFLAGS
#SPF_DEFAULT	= 0
#SPF_ASYNC	= 1 << 0
#SPF_PURGEBEFORESPEAK	= 1 << 1
#SPF_IS_FILENAME	= 1 << 2
#SPF_IS_XML	= 1 << 3
#SPF_IS_NOT_XML	= 1 << 4
#SPF_PERSIST_XML	= 1 << 5
#SPF_NLP_SPEAK_PUNC	= 1 << 6
#SPF_NLP_MASK	= #SPF_NLP_SPEAK_PUNC
#SPF_VOICE_MASK	= #SPF_ASYNC | #SPF_PURGEBEFORESPEAK | #SPF_IS_FILENAME | #SPF_IS_XML | #SPF_IS_NOT_XML | #SPF_NLP_MASK | #SPF_PERSIST_XML
#SPF_UNUSED_FLAGS	= ~#SPF_VOICE_MASK

;- enum SPEVENTLPARAMTYPE
#SPET_LPARAM_IS_UNDEFINED	= 0
#SPET_LPARAM_IS_TOKEN	= #SPET_LPARAM_IS_UNDEFINED + 1
#SPET_LPARAM_IS_OBJECT	= #SPET_LPARAM_IS_TOKEN + 1
#SPET_LPARAM_IS_POINTER	= #SPET_LPARAM_IS_OBJECT + 1
#SPET_LPARAM_IS_STRING	= #SPET_LPARAM_IS_POINTER + 1

;- SPEVENT
Structure SPEVENT Align #PB_Structure_AlignC
	eEventId.w
	elParamType.w
	ulStreamNum.l
	ullAudioStreamOffset.q
	wParam.i
	lParam.i
EndStructure

;- SPVOICESTATUS
Structure SPVOICESTATUS Align #PB_Structure_AlignC
	ulCurrentStream.l
	ulLastStreamQueued.l
	hrLastResult.l
	dwRunningState.l
	ulInputWordPos.l
	ulInputWordLen.l
	ulInputSentPos.l
	ulInputSentLen.l
	lBookmarkId.l
	PhonemeId.a
	VisemeId.l
	dwReserved1.l
	dwReserved2.l
EndStructure

;- ISpProperties
Interface ISpProperties Extends IUnknown
	SetPropertyNum(name.s, value.l)
	GetPropertyNum(name.s, value.l)
	SetPropertyString(name.s, value.s)
	GetPropertyString(name.s, value.i)
EndInterface

;- ISpRecognizer
Interface ISpRecognizer Extends ISpProperties
	SetRecognizer(recognizer.i)
	GetRecognizer(recognizer.i)
	SetInput(input.i, AllowFormatChanges.l)
	GetInputObjectToken(token.i)
	GetInputStream(stream.i)
	CreateRecoContext(NewCtxt.i)
	GetRecoProfile(token.i)
	SetRecoProfile(token.i)
	IsSharedInstance()
	GetRecoState(state.i)
	SetRecoState(state.i)
	GetStatus(status.i)
	GetFormat(WaveFormatType.i, FormatId.i, CoMemWFEX.i)
	IsUISupported(TypeOfUI.s, ExtraData.i, ExtraData.l, Supported.l)
	DisplayUI(hwndParent.i, Title.s, TypeOfUI.s, ExtraData.i, ExtraData.l)
	EmulateRecognition(Phrase.i)
EndInterface

;- ISpNotifySource
Interface ISpNotifySource Extends IUnknown
	SetNotifySink(NotifySink.i)
	SetNotifyWindowMessage(hWnd.i, mdsg.l, wparam.i, lparam.i)
	SetNotifyCallbackFunction(Callback.i, wparam.i, lparam.i)
	SetNotifyCallbackInterface(SpCallback.i, wparam.i, lparam.i)
	SetNotifyWin32Event()
	WaitForNotifyEvent(Milliseconds.l)
	GetNotifyEventHandle()
EndInterface

;- ISpEventSource
Interface ISpEventSource Extends ISpNotifySource
	SetInterest(EventInterest.q, QueuedInterest.q)
	GetEvents(Count.l, EventArray.i, Fetched.i)
	GetInfo(info.i)
EndInterface

;- ISpRecoContext
Interface ISpRecoContext Extends ISpEventSource
	GetRecognizer(Recognizer.i)
	CreateGrammar(GrammarId.q, Grammar.i)
	GetStatus(Status.i)
	GetMaxAlternates(Alternates.i)
	SetMaxAlternates(Alternates.l)
	SetAudioOptions(options.l, AudioFormatId.i, WaveFormatEx.i)
	GetAudioOptions(Options.i, AudioFormatId.i, CoMemWFEX.i)
	DeserializeResult(SerializedResult.i, Result.i)
	Bookmark(Options.l, StreamPosition.q, lparamEvent.i)
	SetAdaptationData(AdaptationData.s, cch.l)
	Pause(Reserved.l)
	Resume(Reserved.l)
	SetVoice(pVoice.i, fAllowFormatChanges.l)
	GetVoice(ppVoice.i)
	SetVoicePurgeEvent(ullEventInterest.q)
	GetVoicePurgeEvent(pullEventInterest.i)
	SetContextState(eContextState.l)
	GetContextState(peContextState.i)
EndInterface

;- ISpGrammarBuilder
Interface ISpGrammarBuilder Extends IUnknown
	ResetGrammar(NewLanguage.w)
	GetRule(pszRuleName.s, dwRuleId.l, dwAttributes.l, fCreateIfNotExist.l, phInitialState.i)
	ClearRule(hState.i)
	CreateNewState(hState.i, phState.i)
	AddWordTransition(hFromState.i, hToState.i, psz.s, pszSeparators.s, eWordType.l, Weight.f, pPropInfo.i)
	AddRuleTransition(hFromState.i, hToState.i, hRule.i, Weight.f, pPropInfo.i)
	AddResource(hRuleState.i, pszResourceName.s, pszResourceValue.s)
	Commit(dwReserved.l)
EndInterface

;- ISpRecoGrammar
Interface ISpRecoGrammar Extends ISpGrammarBuilder
	GetGrammarId(pullGrammarId.i)
	GetRecoContext(ppRecoCtxt.i)
	LoadCmdFromFile(pszFileName, Options.l)
	LoadCmdFromObject(rcid.i, pszGrammarName.s, Options.l)
	LoadCmdFromResource(hModule.i, pszResourceName.s, pszResourceType.s, wLanguage.w, Options.l)
	LoadCmdFromMemory(pGrammar.i, Options.l)
	LoadCmdFromProprietaryGrammar(rguidParam.i, pszStringParam.s, pvDataPrarm.i, cbDataSize.l, Options.l)
	SetRuleState(pszName.s, pReserved.i, NewState.l)
	SetRuleIdState(ulRuleId.l, NewState.l)
	LoadDictation(pszTopicName.s, Options.l)
	UnloadDictation()
	SetDictationState(NewState.l)
	SetWordSequenceData(pText.s, cchText.l, pInfo.i)
	SetTextSelection(pInfo.i)
	IsPronounceable(pszWord.s, pWordPronounceable.i)
	SetGrammarState(eGrammarState.l)
	SaveCmd(pStream.i, ppszCoMemErrorText.i)
	GetGrammarState(peGrammarState.i)
EndInterface

;- ISpPhrase
Interface ISpPhrase Extends IUnknown
	GetPhrase(ppCoMemPhrase.i)
	GetSerializedPhrase(ppCoMemPhrase.i)
	GetText(ulStart.l, ulCount.l, fUseTextReplacements.l, ppszCoMemText.i, pbDisplayAttributes.i)
	Discard(dwValueTypes.l)
EndInterface

;- ISpRecoResult
Interface ISpRecoResult Extends ISpPhrase
	GetResultTimes(pTimes.i)
	GetAlternates(ulStartElement.l, cElements.l, ulRequestCount.l, ppPhrases.i, pcPhrasesReturned.i)
	GetAudio(ulStartElement.l, cElements.l, ppStream.i)
	SpeakAudio(ulStartElement.l, cElements.l, dwFlags.l, pulStreamNumber.i)
	Serialize(ppCoMemSerializedResult.i)
	ScaleAudio(pAudioFormatId.i, pWaveFormatEx.i)
	GetRecoContext(ppRecoContext.i)
EndInterface

;- enum SPVPRIORITY
#SPVPRI_NORMAL	= 0
#SPVPRI_ALERT	= 1 << 0
#SPVPRI_OVER	= 1 << 1

#CLSID_SpVoice$ = "{96749377-3391-11D2-9EE3-00C04F797396}"
#IID_ISpVoice$ = "{6C44DF74-72B9-4992-A1EC-EF996E0422D4}"

;- ISpVoice
Interface ISpVoice Extends ISpEventSource
	SetOutput(pUnkOutput.i, fAllowFormatChanges.l)
	GetOutputObjectToken(ppObjectToken.i)
	GetOutputStream(ppStream.i)
	Pause()
	Resume()
	SetVoice(pToken.i)
	GetVoice(ppToken.i)
	Speak(pwcs.s, dwFlags.l, pulStreamNumber.i)
	SpeakStream(pStream.i, dwFlags.l, pulStreamNumber.i)
	GetStatus(pStatus.i, ppszLastBookmark.i)
	Skip(pItemType.s, lNumItems.l, pulNumSkipped.i)
	SetPriority(ePriority.l)
	GetPriority(pePriority.i)
	SetAlertBoundary(eBoundary.l)
	GetAlertBoundary(peBoundary.i)
	SetRate(RateAdjust.l)
	GetRate(pRateAdjust.i)
	SetVolume(usVolume.w)
	GetVolume(pusVolume.i)
	WaitUntilDone(msTimeout.l)
	SetSyncSpeakTimeout(msTimeout.l)
	GetSyncSpeakTimeout(pmsTimeout.i)
	SpeakCompleteEvent()
	IsUISupported(pszTypeOfUI.s, pvExtraData.i, cbExtraData.l, pfSupported.i)
	DisplayUI(hwndParent.i, pszTitle.s, pszTypeOfUI.s, pvExtraData.i, cbExtraData.l)                                                   
EndInterface

;- IEnumSpObjectTokens
Interface IEnumSpObjectTokens Extends IUnknown
 	Next(celt.l, pelt.i, pceltFetched.i)
  Skip(celt.l)
  Reset()
  Clone(ppEnum.i)
  Item(Index.l, ppToken.i)
  GetCount(pCount.i)           
EndInterface

;- ISpDataKey
Interface ISpDataKey Extends IUnknown
	SetData(pszValueName.s, cbData.l, pData.i)
	GetData(pszValueName.s, pcbData.i, pData.i)
	SetStringValue(pszValueName.s, pszValue.s)
	GetStringValue(pszValueName.s, ppszValue.i)
	SetDWORD(pszValueName.s, dwValue.l)
	GetDWORD(pszValueName.s, pdwValue.i)
	OpenKey(pszSubKeyName.s, ppSubKey.i)
	CreateKey(pszSubKey.s, ppSubKey.i)
	DeleteKey(pszSubKey.s)
	DeleteValue(pszValueName.s)
	EnumKeys(Index.l, ppszSubKeyName.i)
	EnumValues(Index.l, ppszValueName.i)                                 
EndInterface

;- enum SPDATAKEYLOCATION
#SPDKL_DefaultLocation	= 0
#SPDKL_CurrentUser	= 1
#SPDKL_LocalMachine	= 2
#SPDKL_CurrentConfig	= 5

#CLSID_SpObjectTokenCategory$ = "{A910187F-0C7A-45AC-92CC-59EDAFB77B53}"
#IID_ISpObjectTokenCategory$ = "{2D3D3845-39AF-4850-BBF9-40B49780011D}"

;- ISpObjectTokenCategory
Interface ISpObjectTokenCategory Extends ISpDataKey
	SetId(pszCategoryId.s, fCreateIfNotExist.l)
	GetId(ppszCoMemCategoryId.i)
	GetDataKey(spdkl.l, ppDataKey.i)
	EnumTokens(pzsReqAttribs.s, pszOptAttribs.s, ppEnum.i)
	SetDefaultTokenId(pszTokenId.s)
	GetDefaultTokenId(ppszCoMemTokenId.i)          
EndInterface

;- ISpObjectToken
Interface ISpObjectToken Extends ISpDataKey
	SetId(pszCategoryId.s, pszTokenId.s, fCreateIfNotExist.l)
	GetId(ppszCoMemTokenId.i)
	GetCategory(ppTokenCategory.i)
	CreateInstance(pUnkOuter.i, dwClsContext.l, riid.i, ppvObject.i)
	GetStorageFileName(clsidCaller.i, pszValueName.s, pszFileNameSpecifier.s, nFolder.l, ppszFilePath.i)
	RemoveStorageFileName(clsidCaller.i, pszKeyName.s, fDeleteFile.l)
	Remove(pclsidCaller.i)
	IsUISupported(pszTypeOfUI.s, pvExtraData.i, cbExtraData.l, punkObject.i, pfSupported.i)
	DisplayUI(hwndParent.i, pszTitle.s, pszTypeOfUI.s, pvExtraData.i, cbExtraData.l, punkObject.i)
	MatchesAttributes(pszAttributes.s, pfMatches.i)
EndInterface
;}
;{ ;sapihelper.pbi
Declare SpEnumTokens(pszCategoryId.s, pszReqAttribs.s, pszOptAttribs.s, ppEnum.i)
Declare SpGetCategoryFromId(pszCategoryId.s, ppCategory.ISpObjectTokenCategory, fCreateIfNotExist.l = #False)

Procedure SpEnumTokens(pszCategoryId.s, pszReqAttribs.s, pszOptAttribs.s, *ppEnum.INTEGER)
	Define.l hr
	Define.ISpObjectTokenCategory tokenCategory

  hr = SpGetCategoryFromId(pszCategoryId, @tokenCategory)
  If hr = #S_OK
		hr = tokenCategory\EnumTokens(pszReqAttribs, pszOptAttribs, *ppEnum)
	EndIf
	
	ProcedureReturn hr
EndProcedure

Procedure SpGetCategoryFromId(pszCategoryId.s, *ppCategory.INTEGER, fCreateIfNotExist.l = #False)
	Define.IID CLSID_SpObjectTokenCategory
	Define.IID IID_ISpObjectTokenCategory
	Define.ISpObjectTokenCategory tokenCategory
	Define.l hr
	Define.i test
	
	IIDFromString_(#CLSID_SpObjectTokenCategory$, @CLSID_SpObjectTokenCategory)
	IIDFromString_(#IID_ISpObjectTokenCategory$, @IID_ISpObjectTokenCategory)

	hr = CoCreateInstance_(@CLSID_SpObjectTokenCategory, #Null, #CLSCTX_INPROC_SERVER, @IID_ISpObjectTokenCategory, @tokenCategory)
	If hr = #S_OK
		hr = tokenCategory\SetId(pszCategoryId, fCreateIfNotExist)
		If hr = #S_OK
			*ppCategory\i = tokenCategory
		
		Else
			tokenCategory\Release()
		EndIf 
	EndIf 
	
	ProcedureReturn hr
EndProcedure

Procedure SpClearEvent(*pe.SPEVENT)
	Protected.IUnknown pUnk
	
	If *pe\elParamType <> #SPEI_UNDEFINED
		If *pe\elParamType = #SPET_LPARAM_IS_POINTER Or *pe\elParamType = #SPET_LPARAM_IS_STRING
			CoTaskMemFree_(*pe\lParam)
		
		ElseIf *pe\elParamType = #SPET_LPARAM_IS_TOKEN Or *pe\elParamType = #SPET_LPARAM_IS_OBJECT
			pUnk = *pe\lParam
			pUnk\Release()
		EndIf 
	EndIf 
	
	FillMemory(*pe, 0, SizeOf(SPEVENT))
EndProcedure
;}

declare sapi_Stop()

global _sapi_cbVoices.i,_sapi_iVoice.i,_sapi_cpEnum.IEnumSpObjectTokens,_sapi_voiceToken.ISpObjectToken
global _sapi_CLSID_SpVoice.IID,_sapi_IID_ISpVoice.IID,_sapi_pVoice.ISpVoice,_sapi_hr.l,_sapi_voiceId.i
global _sapi_isVoiceRunning.b,_sapi_isVoicePaused.b,_sapi_BookmarkReached$
global _sapi_addr_DefSubclassProc.i,_sapi_addr_SetWindowSubclass.i,_sapi_addr_RemoveWindowSubclass.i,_Comctl32.i
global _sapi_initialized.b=0,_sapi_hMainWnd.i,_sapi_InternalUserMess.i,_sapi_UserMessStart.i,_sapi_UserMessStop.i,_sapi_UserMessBookmarkReached.i,_sapi_UserMessWordBoundary.i
global _sapi_CurrentWordLen.l,_sapi_CurrentWordPos.l,_sapi_CurrentText$

Procedure sapi_SubclassProc(hWnd.i,uMsg.i,wParam.i,lParam.i,uIdSubclass.i,dwRefData.i)
  protected erg,ret
  protected eventItem.SPEVENT
  erg=999 ;if kept =999 the original window procedure will be called at the end, otherwise not
  if uMsg=_sapi_InternalUserMess
    While _sapi_pVoice\GetEvents(1, @eventItem, #Null) = #S_OK
      Select eventItem\eEventId 
        Case #SPEI_START_INPUT_STREAM
          _sapi_isVoiceRunning = #True
          _sapi_BookmarkReached$=""
          if _sapi_UserMessStart<>0
            sendmessage_(_sapi_hMainWnd,_sapi_UserMessStart,0,0)
          endif
          Break
        Case #SPEI_END_INPUT_STREAM
          _sapi_isVoiceRunning = #False
          _sapi_BookmarkReached$=""
          If _sapi_isVoicePaused
            _sapi_pVoice\Resume()
            _sapi_isVoicePaused = #False
          EndIf
          if _sapi_UserMessStop<>0
            sendmessage_(_sapi_hMainWnd,_sapi_UserMessStop,0,0)
          endif
          Break
        Case #SPEI_TTS_BOOKMARK ;e.g. »<bookmark mark="bookmark_one"/>« in the text
          if eventItem\elParamType=#SPET_LPARAM_IS_STRING
            _sapi_BookmarkReached$=PeekS(eventItem\lParam) ;PeekS(PeekI(lParam),-1)
          else
            _sapi_BookmarkReached$=str(PeekL(eventItem\wParam))
          endif
          if _sapi_UserMessBookmarkReached<>0
            sendmessage_(_sapi_hMainWnd,_sapi_UserMessBookmarkReached,0,0)
          endif
          Break
        Case #SPEI_WORD_BOUNDARY
          _sapi_CurrentWordLen=eventItem\wParam
          _sapi_CurrentWordPos=eventItem\lParam
          if _sapi_UserMessWordBoundary<>0
            sendmessage_(_sapi_hMainWnd,_sapi_UserMessWordBoundary,_sapi_CurrentWordPos,_sapi_CurrentWordLen)
          endif
      EndSelect
    Wend 
    SpClearEvent(@eventItem)
  endif
  ;{ ;return value, call original window procedure if appropriate
  If erg=999 ;call original window procedure
    ProcedureReturn CallFunctionFast(_sapi_addr_DefSubclassProc,hWnd,uMsg,wParam,lParam)
  Else ;return special value and *do not* call original window procedure
    ProcedureReturn erg
  EndIf
  ;}
EndProcedure ;sapi_SubClassProc

Procedure sapi_Init(hMainWnd.i,UserMessStart.i=0,UserMessStop.i=0,UserMessBookmarkReached.i=0,UserMessWordBoundary.i=0,InternalUserMess.i=#WM_USER) ;call once at the beginning, supply ID (MS-Windows handle) of main program window
  protected ret.l,err.l
  ret=1
  err=0
  if _sapi_initialized=0
    _sapi_hMainWnd=hMainWnd
    _sapi_InternalUserMess=InternalUserMess
    _sapi_UserMessStart=UserMessStart
    _sapi_UserMessStop=UserMessStop
    _sapi_UserMessBookmarkReached=UserMessBookmarkReached
    _sapi_UserMessWordBoundary=UserMessWordBoundary
    _sapi_isVoiceRunning = #False
    _sapi_isVoicePaused = #False
    _sapi_BookmarkReached$=""
    ;{ ;COM-Init
    CoInitialize_(0)
    IIDFromString_(#CLSID_SpVoice$, @_sapi_CLSID_SpVoice)
    IIDFromString_(#IID_ISpVoice$, @_sapi_IID_ISpVoice)
    _sapi_hr = CoCreateInstance_(@_sapi_CLSID_SpVoice, #Null, #CLSCTX_INPROC_SERVER, @_sapi_IID_ISpVoice, @_sapi_pVoice)
    If _sapi_hr <> #S_OK
      ;Debug "Failed to create voice object"
      ;End
      ret=0
    Else
      _Comctl32=OpenLibrary(#PB_Any,"Comctl32.dll") ;neccessary because the subclassing functions aren't included in PB with an underscore at their end
      _sapi_addr_DefSubclassProc=GetFunction(_Comctl32,"DefSubclassProc")
      _sapi_addr_SetWindowSubclass=GetFunction(_Comctl32,"SetWindowSubclass")
      _sapi_addr_RemoveWindowSubclass=GetFunction(_Comctl32,"RemoveWindowSubclass")
      _sapi_cbVoices=ComboBoxGadget(#PB_Any,-10,0,0,0)
      ;{ ;get voices
      If SpEnumTokens(#SPCAT_VOICES2, "", "", @_sapi_cpEnum) = #S_OK ;VOICES2 first, because normally they contain more entries than VOICES 1
        _sapi_iVoice = 0
        While _sapi_cpEnum\Next(1, @_sapi_voiceToken, #Null) = #S_OK
          If _sapi_voiceToken\GetId(@_sapi_voiceId) = #S_OK
            AddGadgetItem(_sapi_cbVoices, _sapi_iVoice, GetFilePart(PeekS(_sapi_voiceId)))
            SetGadgetItemData(_sapi_cbVoices, _sapi_iVoice, _sapi_voiceToken)
;messagebox_(0,str(_sapi_iVoice)+"   "+str(_sapi_voiceToken)+"   "+PeekS(_sapi_voiceId),"",0)
          EndIf 
          _sapi_iVoice + 1
        Wend 
        _sapi_cpEnum\Release()
        SetGadgetState(_sapi_cbVoices, 0)
      Else
        ;Debug "Failed to enumerate voices"
        ;End 
        ;err=err+1
      EndIf
      if CountGadgetItems(_sapi_cbVoices)=0
        ;err=err+1
        If SpEnumTokens(#SPCAT_VOICES1, "", "", @_sapi_cpEnum) = #S_OK	
          _sapi_iVoice = 0
          While _sapi_cpEnum\Next(1, @_sapi_voiceToken, #Null) = #S_OK
            If _sapi_voiceToken\GetId(@_sapi_voiceId) = #S_OK
              AddGadgetItem(_sapi_cbVoices, _sapi_iVoice, GetFilePart(PeekS(_sapi_voiceId)))
              SetGadgetItemData(_sapi_cbVoices, _sapi_iVoice, _sapi_voiceToken)
            EndIf 
            _sapi_iVoice + 1
          Wend 
          _sapi_cpEnum\Release()
          SetGadgetState(_sapi_cbVoices, 0)
        Else
          ;Debug "Failed to enumerate voices"
          ;End 
          ;err=err+1
        EndIf 
        if CountGadgetItems(_sapi_cbVoices)=0
          ret=-2
        endif
      else ;add Windows XP voices if available (MSMike, MSSam, MSMary) --> see here (2nd reply!): https://answers.microsoft.com/en-us/windows/forum/all/how-to-get-microsoft-sam-on-windows-7/20b2beb4-e927-4be5-90d3-45c3af1bf2ce
        If SpEnumTokens(#SPCAT_VOICES1, "", "", @_sapi_cpEnum) = #S_OK
          _sapi_iVoice = 0
          While _sapi_cpEnum\Next(1, @_sapi_voiceToken, #Null) = #S_OK
            If _sapi_voiceToken\GetId(@_sapi_voiceId) = #S_OK
              if (ucase(GetFilePart(PeekS(_sapi_voiceId)))="MSMIKE") or (ucase(GetFilePart(PeekS(_sapi_voiceId)))="MSSAM") or (ucase(GetFilePart(PeekS(_sapi_voiceId)))="MSMARY")
                AddGadgetItem(_sapi_cbVoices, _sapi_iVoice, GetFilePart(PeekS(_sapi_voiceId)))
                SetGadgetItemData(_sapi_cbVoices, _sapi_iVoice, _sapi_voiceToken)
              endif
            EndIf 
            _sapi_iVoice + 1
          Wend 
          _sapi_cpEnum\Release()
          SetGadgetState(_sapi_cbVoices, 0)
        Else
          ;Debug "Failed to enumerate voices"
          ;End 
          ;err=err+1
        EndIf
      endif
      ;}
    EndIf
    ;}
  else
    ret=-1 ;already initialized
  endif
  if (ret=1) or (ret=-2)
    CallFunctionFast(_sapi_addr_SetWindowSubclass,hMainWnd,@sapi_SubclassProc(),1,0)
    _sapi_pVoice\SetInterest(#SPFEI_ALL_EVENTS, #SPFEI_ALL_EVENTS)
    _sapi_pVoice\SetNotifyWindowMessage(hMainWnd, _sapi_InternalUsermess, 0, 0 )
    _sapi_initialized=1
  endif
  ProcedureReturn ret
EndProcedure ;sapi_Init

Procedure sapi_DeInit() ;call once at the end
  protected iVoice.l,erg.l
  erg=1
  if _sapi_initialized
    if _sapi_isVoiceRunning
      sapi_Stop()
    endif
    ;{ ;release voices
    For iVoice = 0 To CountGadgetItems(_sapi_cbVoices) - 1
      _sapi_voiceToken = GetGadgetItemData(_sapi_cbVoices, iVoice)
      If _sapi_voiceToken
        _sapi_voiceToken\Release()
      EndIf 
    Next 
    If _sapi_pVoice : _sapi_pVoice\Release() : EndIf 
    ;}
    CallFunctionFast(_sapi_addr_RemoveWindowSubclass,_sapi_hMainWnd,@sapi_SubclassProc(),1)
    CloseLibrary(_Comctl32)
    if _sapi_cbVoices
      FreeGadget(_sapi_cbVoices)
      _sapi_cbVoices=0
    endif
    _sapi_initialized=0
  else
    erg=-1
  endif
  ProcedureReturn erg
EndProcedure ;sapi_DeInit

Procedure sapi_Say(Text$)
  protected erg.i
  erg=0
  if _sapi_initialized
    _sapi_CurrentText$=Text$
    _sapi_CurrentWordPos=0
    _sapi_CurrentWordLen=0
    If _sapi_isVoiceRunning = #False
      erg=_sapi_pVoice\Speak(Text$, #SPF_IS_XML | #SPF_ASYNC, #Null)
      if erg=0
        erg=1
      else
        erg=0
      endif
      _sapi_BookmarkReached$=""
      _sapi_isVoiceRunning=#True
    Else ;If _sapi_isVoicePaused = #True
      ;{ ;original logic (resume)
      ;erg=_sapi_pVoice\Resume()
      ;_sapi_isVoicePaused = #False
      ;}
      sapi_Stop()
      erg=_sapi_pVoice\Speak(Text$, #SPF_IS_XML | #SPF_ASYNC, #Null)
      if erg=0
        erg=1
      else
        erg=0
      endif
      _sapi_BookmarkReached$=""
      _sapi_isVoiceRunning=#True
      _sapi_isVoicePaused=#False
    EndIf
  else
    erg=-1
  endif
  ProcedureReturn erg
EndProcedure ;sapi_Say

Procedure sapi_Pause()
  protected erg.i
  erg=0
  if _sapi_initialized
    If (_sapi_isVoiceRunning = #True) And (_sapi_isVoicePaused = #False)
      erg=_sapi_pVoice\Pause()
      if erg=0
        erg=1
      else
        erg=0
      endif
      _sapi_isVoicePaused = #True
    EndIf 
  else
    erg=-1
  endif
  ProcedureReturn erg
EndProcedure ;sapi_Pause

Procedure sapi_Resume()
  protected erg.i
  erg=0
  if _sapi_initialized
    If (_sapi_isVoiceRunning = #True) And (_sapi_isVoicePaused = #True)
      erg=_sapi_pVoice\Resume()
      if erg=0
        erg=1
      else
        erg=0
      endif
      _sapi_isVoicePaused = #False
    EndIf 
  else
    erg=-1
  endif
  ProcedureReturn erg
EndProcedure ;sapi_Resume

Procedure sapi_Stop()
  protected erg.i
  protected eventItem.SPEVENT
  erg=0
  if _sapi_initialized
    If _sapi_isVoiceRunning
      if _sapi_isVoicePaused
        sapi_Resume()
      endif
      erg=_sapi_pVoice\Speak("", #SPF_PURGEBEFORESPEAK, #Null)
      ;erg=_sapi_pVoice\Skip("Sentence", Len(_CurrentText$), #Null)
      if erg=0
        erg=1
      else
        erg=0
      endif
      ;Alt method to stop
      ;app\pVoice\Skip("Sentence", Len(GetGadgetText(app\edText)), #Null)
      _sapi_isVoiceRunning=#False
      _sapi_isVoicePaused=#False
      _sapi_BookmarkReached$=""
      _sapi_CurrentWordPos=0
      _sapi_CurrentWordLen=0
      while _sapi_pVoice\GetEvents(1, @eventItem, #Null)=#S_OK ;purge all events that would otherwise be stuck in the loop until the next readout commences
      wend
    EndIf 
  else
    erg=-1
  endif
  ProcedureReturn erg
EndProcedure ;sapi_Stop

Procedure.s sapi_GetVoiceNames() ;returns the names of all installed voices separated by "|"
  protected erg$,i.l
  erg$=""
  if _sapi_initialized
    for i=0 to CountGadgetItems(_sapi_cbVoices)-1
      if erg$<>""
        erg$=erg$+"|"
      endif
      erg$=erg$+GetGadgetItemText(_sapi_cbVoices,i)
    next i
  else
    erg$="-1"
  endif
  ProcedureReturn erg$
EndProcedure ;sapi_GetVoiceNames

Procedure sapi_SetVoice(VoiceName$) ;VoiceName$=one of the names returned by sapi_GetVoiceNames()
  protected erg.i,voiceToken.ISpObjectToken
  erg=0
  if _sapi_initialized
    if VoiceName$<>""
      SetGadgetText(_sapi_cbVoices,VoiceName$)
      voiceToken = GetGadgetItemData(_sapi_cbVoices, GetGadgetState(_sapi_cbVoices))
      If voiceToken
        _sapi_pVoice\SetVoice(voiceToken)
        erg=1
      EndIf 
    endif
  else
    erg=-1
  endif
  ProcedureReturn erg
EndProcedure ;sapi_SetVoice

Procedure sapi_SetSpeed(Speed.b=0) ;-10 to +10; default=0
  protected erg.i
  erg=0
  if _sapi_initialized
    if (Speed>=-10) and (Speed<=10)
      erg=_sapi_pVoice\SetRate(Speed)
      erg=1-erg
    endif
  else
    erg=-1
  endif
  ProcedureReturn erg
EndProcedure ;sapi_SetSpeed

Procedure sapi_SetVolume(Volume.b=100) ;0 to 100; default=100
  protected erg.i
  erg=0
  if _sapi_initialized
    if (Volume>=0) and (Volume<=100)
      erg=_sapi_pVoice\SetVolume(Volume)
      erg=1-erg
    endif
  else
    erg=-1
  endif
  ProcedureReturn erg
EndProcedure ;sapi_SetVolume

Procedure sapi_IsVoiceRunning() ;returns 1 if text is being read out (even if paused), 0 otherwise
  protected erg.i
  erg=0
  if _sapi_initialized
    erg=_sapi_isVoiceRunning
  else
    erg=-1
  endif
  ProcedureReturn erg
EndProcedure ;sapi_IsVoiceRunning

Procedure sapi_IsVoicePaused() ;returns 1 if readout is paused, 0 otherwise
  protected erg.i
  erg=0
  if _sapi_initialized
    erg=_sapi_isVoicePaused
  else
    erg=-1
  endif
  ProcedureReturn erg
EndProcedure ;sapi_IsVoicePaused

Procedure.s sapi_GetLastBookmark() ;e.g. »<bookmark mark="bookmark_one"/>« in the text
protected erg$
  erg$=""
  if _sapi_initialized
    erg$=_sapi_BookmarkReached$
  else
    erg$="-1"
  endif
  ProcedureReturn erg$
EndProcedure ;sapi_GetLastBookmark

Procedure sapi_GetCurrentWordPos(TakeChr13IntoAccount.i=0)
  protected s$
  if TakeChr13IntoAccount
    s$=left(_sapi_CurrentText$,_sapi_CurrentWordPos)
    ProcedureReturn _sapi_CurrentWordPos-CountString(s$,chr(13))
  else
    ProcedureReturn _sapi_CurrentWordPos
  endif
EndProcedure ;sapi_GetCurrentWordPos

Procedure sapi_GetCurrentWordLen()
  ProcedureReturn _sapi_CurrentWordLen
EndProcedure ;sapi_GetCurrentWordLen

Code: Select all

;sapi-all test
;see here: https://www.purebasic.fr/english/viewtopic.php?t=71402

EnableExplicit

XIncludeFile "sapi-all.pbi"
global _nWnd,_hWnd,_event,_progend.b,_gadgetevent,_nBend,_menuitem,_nBsay,_nETextToSay,_nBpause,_nBresume,_nBstop,_nBSpeedUp,_nBSpeedDown,_nBVolumeUp,_nBVolumeDown,_nCBHighlightWords
global _Speed.b,_Volume.b,_nTstatus,_nCBvoices,_s$,_i,_OldPlaying.b,_OldPaused.b,_OldBookmarkReached$,_nThunder.i,_OldCurrentWordPos.l,_Sel1.l,_Sel2.l

_nWnd=OpenWindow(#PB_Any,100,100,600,430,"sapi-Test", #PB_Window_SystemMenu | #PB_Window_ScreenCentered)
_hWnd=WindowID(_nWnd)

InitSound()
if FileSize("thunder1.wav")>0
  _nThunder=LoadSound(#PB_Any,"thunder1.wav")
endif

sapi_Init(_hWnd) ;call once at the beginning, supply ID (MS-Windows handle) of main program window
;_Speed=1 ;I set this to 1 because in my eyes the default speed (0) is a little bit too slow (at least with the German voice)
;sapi_SetSpeed(_Speed) ;range: -10 to 10, 0=default
_Volume=100

;{ ;create GUI
_nTstatus=CreateStatusBar(#PB_Any,_hWnd)
AddStatusBarField(85)
AddStatusBarField(85)
AddStatusBarField(85)
AddStatusBarField(85)
AddStatusBarField(250)
StatusBarText(_nTstatus,0,"   Speed="+str(_Speed))
StatusBarText(_nTstatus,1,"Volume="+str(_Volume)+"%")
StatusBarText(_nTstatus,2,"Playing="+str(sapi_IsVoiceRunning()))
StatusBarText(_nTstatus,3,"Paused="+str(sapi_IsVoicePaused()))
StatusBarText(_nTstatus,4,"Bookmark="+_sapi_BookmarkReached$)
_nCBvoices=ComboBoxGadget(#PB_Any,10,10,580,23)
_s$=sapi_GetVoiceNames()
for _i=1 to CountString(_s$,"|")+1
  AddGadgetItem(_nCBvoices,-1,StringField(_s$,_i,"|"))
next _i
SetGadgetState(_nCBvoices,0)
_nETextToSay=EditorGadget(#PB_Any,10,44,580,241,#PB_Editor_WordWrap)
;SetGadgetText(_nETextToSay,"Enter text here. This is a mere sample text.")
SetGadgetText(_nETextToSay,""+
                           "There was a thunderstorm approaching,<bookmark mark="+chr(34)+"Thunder"+chr(34)+"/><silence msec="+chr(34)+"2500"+chr(34)+"/>which everyone was afraid of."+chr(13)+
                           "Demonstration of different languages as long as they're installed:"+chr(13)+
                           "<lang langid="+chr(34)+"409"+chr(34)+">This is english.</lang>"+chr(13)+
                           "<silence msec="+chr(34)+"250"+chr(34)+"/>"+chr(13)+
                           "<lang langid="+chr(34)+"407"+chr(34)+">Das ist deutsch.</lang>"+chr(13)+
                           "<lang langid="+chr(34)+"40C"+chr(34)+">C'est francais.</lang>"+chr(13)+
                           "<silence msec="+chr(34)+"500"+chr(34)+"/>"+chr(13)+
                           "Now <emph>this</emph> word is emphasized."+chr(13)+
                           "<pitch absmiddle="+chr(34)+"-10"+chr(34)+">Very low voice.</pitch>)"+chr(13)+
                           "<pitch absmiddle="+chr(34)+"10"+chr(34)+">Very high voice.</pitch>)"+chr(13)+
                           "<pitch absmiddle="+chr(34)+"0"+chr(34)+">Normal voice.</pitch>)"+chr(13)+
                           "<rate absspeed="+chr(34)+"-7"+chr(34)+"><spell>spell it out</spell></rate>"+chr(13)+
                           "<silence msec="+chr(34)+"500"+chr(34)+"/>"+chr(13)+
                           "<volume level="+chr(34)+"50"+chr(34)+">Hushed voice.</volume>"+chr(13)+
                           "<volume level="+chr(34)+"100"+chr(34)+">Loud voice.</volume>"+chr(13)+
                           "")
_nBsay=ButtonGadget(#PB_Any,10,295,100,30,"Say text")
_nBstop=ButtonGadget(#PB_Any,120,295,100,30,"Stop")
_nBpause=ButtonGadget(#PB_Any,230,295,100,30,"Pause")
_nBresume=ButtonGadget(#PB_Any,340,295,100,30,"Resume")
_nBSpeedDown=ButtonGadget(#PB_Any,10,335,100,30,"Speed down")
_nBSpeedUp=ButtonGadget(#PB_Any,120,335,100,30,"Speed up")
_nBVolumeDown=ButtonGadget(#PB_Any,230,335,100,30,"Volume down")
_nBVolumeUp=ButtonGadget(#PB_Any,340,335,100,30,"Volume up")
_nCBHighlightWords=CheckBoxGadget(#PB_Any,10,375,200,20,"highlight words")
SetGadgetState(_nCBHighlightWords,1)
_nBend=ButtonGadget(#PB_Any,490,335,100,30,"Quit")
;}

;{ ;message loop
_progend=0
_OldPlaying=sapi_IsVoiceRunning()
_OldPaused=sapi_IsVoicePaused()
while _progend=0
  _event=WaitWindowEvent()
  _gadgetevent=0
  if _event=#PB_Event_Gadget
    _gadgetevent=EventGadget()
  elseif _event=#PB_Event_Menu
    _MenuItem=EventMenu()
  endif
  if _OldBookmarkReached$<>sapi_GetLastBookmark()
    _OldBookmarkReached$=sapi_GetLastBookmark()
    StatusBarText(_nTstatus,4,"Bookmark="+_OldBookmarkReached$)
    if _nThunder
      if _OldBookmarkReached$="Thunder"
        PlaySound(_nThunder)
      endif
    endif
  endif
  
  if (sapi_IsVoiceRunning()<>_OldPlaying) or (sapi_IsVoicePaused()<>_OldPaused)
    StatusBarText(_nTstatus,2,"Playing="+str(sapi_IsVoiceRunning())):StatusBarText(_nTstatus,3,"Paused="+str(sapi_IsVoicePaused()))
    _OldPlaying=sapi_IsVoiceRunning():_OldPaused=sapi_IsVoicePaused()
  endif
  if _gadgetevent=_nBsay
    if _nThunder
      StopSound(_nThunder)
    endif
    _OldCurrentWordPos=-1
    sendmessage_(GadgetID(_nETextToSay),#EM_SETSEL,0,0)
    sapi_Say(GetGadgetText(_nETextToSay))
  elseif _gadgetevent=_nBpause
    if (sapi_IsVoiceRunning()=1) and (sapi_IsVoicePaused()=0)
      sapi_Pause()
    endif
  elseif _gadgetevent=_nBresume
    if (sapi_IsVoiceRunning()=1) and (sapi_IsVoicePaused()=1)
      sapi_Resume()
    endif
  elseif _gadgetevent=_nBstop
    if sapi_IsVoiceRunning()=1
      if _nThunder
        StopSound(_nThunder)
      endif
      sapi_Stop()
      sendmessage_(GadgetID(_nETextToSay),#EM_SETSEL,0,0)
    endif
  elseif _gadgetevent=_nBSpeedUp
    if _Speed<10
      _Speed=_Speed+1
      sapi_SetSpeed(_Speed)
      StatusBarText(_nTstatus,0,"   Speed="+str(_Speed))
    endif
  elseif _gadgetevent=_nBSpeedDown
    if _Speed>-10
      _Speed=_Speed-1
      sapi_SetSpeed(_Speed)
      StatusBarText(_nTstatus,0,"   Speed="+str(_Speed))
    endif
  elseif _gadgetevent=_nBVolumeUp
    if _Volume<=90
      _Volume=_Volume+10
      sapi_SetVolume(_Volume)
      StatusBarText(_nTstatus,1,"Volume="+str(_Volume)+"%")
    endif
  elseif _gadgetevent=_nBVolumeDown
    if _Volume>=10
      _Volume=_Volume-10
      sapi_SetVolume(_Volume)
      StatusBarText(_nTstatus,1,"Volume="+str(_Volume)+"%")
    endif
  elseif _gadgetevent=_nCBvoices
    if EventType()= #PB_EventType_Change   
      sapi_SetVoice(GetGadgetText(_nCBvoices))
    endif
  elseif (_event=#PB_Event_CloseWindow) or (_gadgetevent=_nBend)
    if _nThunder
      StopSound(_nThunder)
    endif
    sendmessage_(GadgetID(_nETextToSay),#EM_SETSEL,0,0)
    _progend=1
  endif
  if GetGadgetState(_nCBHighlightWords)
    if (_OldCurrentWordPos<>sapi_GetCurrentWordPos(1)) and (sapi_GetCurrentWordLen()<>0)
      _OldCurrentWordPos=sapi_GetCurrentWordPos(1)
      sendmessage_(GadgetID(_nETextToSay),#EM_SETSEL,_OldCurrentWordPos,_OldCurrentWordPos+sapi_GetCurrentWordLen())
    endif
  else
    sendmessage_(GadgetID(_nETextToSay),#EM_GETSEL,@_Sel1,@_Sel2)
    if _Sel1<>_Sel2
      sendmessage_(GadgetID(_nETextToSay),#EM_SETSEL,0,0)
    endif
  endif
  
  _gadgetevent=0
  _MenuItem=0
wend
;}

sapi_DeInit() ;call once at the end

if _nThunder
  FreeSound(_nThunder)
endif
CloseWindow(_nWnd)
end
Last edited by Jens-Arne on Tue Feb 25, 2025 4:34 pm, edited 52 times in total.
Quin
Addict
Addict
Posts: 1122
Joined: Thu Mar 31, 2022 7:03 pm
Location: Colorado, United States
Contact:

Re: MS Speech SAPI Text to speech COM (Windows only)

Post by Quin »

Jens-Arne wrote: Sun Jan 12, 2025 12:58 pm Really great work!

I have simplified things further so you don't have to bother about anything but to supply the text to be read out. Just one pbi needed in which everything is included.

These are the only functions you need to do all the work:

Code: Select all

sapi_Init(hMainWnd.i) ;call once at the beginning, supply ID (MS-Windows handle) of main program window
sapi_DeInit() ;call once at the end
sapi_Say(Text$)
sapi_Pause()
sapi_Resume()
sapi_Stop()
sapi_GetVoiceNames() ;returns the names of all installed voices separated by "|"
sapi_SetVoice(VoiceName$) ;VoiceName$=one of the names returned from sapi_GetVoiceNames()
Very nice work! Just tested and this all works on Windows 10. Thanks for sharing! :)
Jens-Arne
User
User
Posts: 43
Joined: Sun Feb 04, 2024 11:09 am

Re: MS Speech SAPI Text to speech COM (Windows only)

Post by Jens-Arne »

I've added a second optional parameter for sapi_Init(), see updated description above. This way you can choose what user message number the pbi uses internally in its subclass function should #WM_USER already be occupied by your own program.

Furthermore I've changed the edit control in the example to be an EditorGadget rather than a StringGadget (that's the way it was supposed to be from the beginning).
Jens-Arne
User
User
Posts: 43
Joined: Sun Feb 04, 2024 11:09 am

Re: MS Speech SAPI Text to speech COM (Windows only)

Post by Jens-Arne »

sapi_SetSpeed & sapi_SetVolume added, example updated (now with ComboBox for voice selection).
Use functions instead of variables now to determine status of playing and pausing.
This should be it - for the time being... ;-)
User avatar
minimy
Enthusiast
Enthusiast
Posts: 552
Joined: Mon Jul 08, 2013 8:43 pm
Location: off world

Re: MS Speech SAPI Text to speech COM (Windows only)

Post by minimy »

Thanks Justin! very nice code! Thanks for share.
If translation=Error: reply="Sorry, Im Spanish": Endif
Jens-Arne
User
User
Posts: 43
Joined: Sun Feb 04, 2024 11:09 am

Re: MS Speech SAPI Text to speech COM (Windows only)

Post by Jens-Arne »

I've changed the source of the voices so that more of them should be found now.
Jens-Arne
User
User
Posts: 43
Joined: Sun Feb 04, 2024 11:09 am

Re: MS Speech SAPI Text to speech COM (Windows only)

Post by Jens-Arne »

Added support for XP-Voices, namely Microsoft Sam (and Mike and Mary)!

See here how to get these vintage voices (2nd reply!): https://answers.microsoft.com/en-us/win ... c3af1bf2ce

It worked for me to install these voices the way that is stated there. Cool to have Sam back once more. Soi soi soi soi soi... :D
Jens-Arne
User
User
Posts: 43
Joined: Sun Feb 04, 2024 11:09 am

Re: MS Speech SAPI Text to speech COM (Windows only)

Post by Jens-Arne »

I've changed the default text in the test program to demonstrate a number of things that are possible with SAPI.

For this purpose I've added a new function sapi_GetLastBookmark() which returns the name of the last bookmark reached in the text. This way one could trigger certain program functions on reaching a given bookmark if one liked (e.g. playing a thunderbolt sound upon reaching the part of the story telling that there was a thunderstorm looming).
User avatar
gurj
Enthusiast
Enthusiast
Posts: 693
Joined: Thu Jan 22, 2009 3:48 am
Location: china
Contact:

Re: MS Speech SAPI Text to speech COM (Windows only)

Post by gurj »

thanks!
Can you increase the function: Automatically select the word being read.
my pb for chinese:
http://ataorj.ys168.com
Post Reply