Announcement

Collapse
No announcement yet.

Slow SAPI

Collapse
X
  • Filter
  • Time
  • Show
Clear All
new posts

  • Slow SAPI

    I would like to be able to click on an item in the ListBox and have the item read out loud immediately. Then, if I click another one quickly, I'd like the first speech to stop immediately and the new item to be read immediately.

    But not only is there a noticeable time delay before SAPI speaks, but it won't stop quickly either.

    I tried a few variations on this code, but speed of response doesn't seem to be SAPI's strong suit!

    Code:
    #Compile Exe
    #Dim All
    %Unicode=1
    
    #Debug Error On
    #Debug Display On
    
    #Include "win32api.inc"
    #Include "sapi.inc"
    
    %IDC_ListBox = 500
    %Msg_SAPI_Event  = %WM_User+1
    
    Global psp As ISpVoice, TextToSpeak$$, hDlg,hThread As Dword
    
    Function PBMain() As Long
       Dim MyArray(7) As String
       Array Assign MyArray() = "zero", "one", "two", "three","four","five","six","seven"
       Dialog Default Font "Tahoma", 21,1
       Dialog New Pixels, 0, "ListBox Test",300,300,150,300, %WS_OverlappedWindow, 0 To hDlg
       Control Add ListBox, hDlg, %IDC_ListBox, MyArray(), 5,5,190,300
       Dialog Show Modal hDlg Call DlgProc
    End Function
    
    CallBack Function DlgProc() As Long
       Local iRow As Long, temp$
       Select Case Cb.Msg
          Case %WM_InitDialog
             pSp = NewCom "SAPI.SpVoice"
             pSp.SetInterest(SPFEI(%SPEI_WORD_BOUNDARY) Or SPFEI(%SPEI_END_INPUT_STREAM), SPFEI(%SPEI_WORD_BOUNDARY) Or SPFEI(%SPEI_END_INPUT_STREAM))
             pSp.SetNotifyWindowMessage(hDlg, %MSG_SAPI_EVENT, 0, 0)
          Case %WM_Command
             Select Case Cb.Ctl
                Case %IDC_ListBox
                   Select Case Cb.CtlMsg
                      Case %LBN_SelChange
                         ListBox Get Select hDlg, %IDC_ListBox To iRow
                         ListBox Get Text hDlg, %IDC_ListBox, iRow To TextToSpeak$$
                         Thread Create SimpleReadText(0) To hThread
                         Thread Close hThread To hThread
                   End Select
             End Select
       End Select
    End Function
    
    Thread Function SimpleReadText(ByVal X As Long) As Long
       pSp.Speak(ByVal StrPtr(TextToSpeak$$), %SPF_PurgeBeforeSpeak, ByVal %Null)
    End Function

  • #2
    Sounds ok to me on my computer.
    What sort of delay do you hear?

    Comment


    • #3
      Hi Paul!

      Perhaps a quarter+ second delay from when I click on the listbox to when the first word starts speaking. Then all words I subsequently click (quickly) on must finish before the next word starts, meaning I can click on several words and have over 1s+ before the last word even starts.

      I would like a click on a word to immediately cancel any speaking in progress and immediately begin speaking of the most recent click.

      Comment


      • #4
        I should say that it's more likely I'll use the down/up arrow to change the selection and to speak the new selection. So it will be easy to click quickly using the keyboard.

        Comment


        • #5
          Hey Gary,
          asynchronous might be the word to look for...

          Code:
          #COMPILE EXE 'Use José's includes
          #DIM ALL
          #REGISTER NONE
          #INCLUDE "Win32Api.inc"
          #INCLUDE "SApi.inc"
          '#RESOURCE ".pbr"
          
          %Msg_SAPI_Event = %WM_User + 1
          
          GLOBAL hDlg AS DWORD
          $AppName   = "Speaking list"
          %Listbox01 = 101
          '_____________________________________________________________________________
          
          CALLBACK FUNCTION DlgProc
           STATIC psp   AS ISpVoice
           STATIC ws1   AS WSTRINGZ * 64
           LOCAL  index AS LONG
          
           SELECT CASE CBMSG
          
             CASE %WM_INITDIALOG
               pSp = NEWCOM "SAPI.SpVoice"
          
             CASE %WM_COMMAND
               SELECT CASE CBCTL
                 CASE %Listbox01
                   SELECT CASE CBCTLMSG
                     CASE %LBN_SELCHANGE
                       LISTBOX GET SELECT hDlg, %Listbox01 TO index
                       LISTBOX GET TEXT hDlg, %Listbox01, index TO ws1
                       pSp.Speak(ws1, %SPF_ASYNC OR %SPF_PURGEBEFORESPEAK, BYVAL %NULL)
                   END SELECT
               END SELECT
            END SELECT
          
          END FUNCTION
          '_____________________________________________________________________________
          
          FUNCTION PBMAIN()
           LOCAL hIcon AS DWORD
          
           DIALOG FONT "Segoe UI", 9
           DIALOG NEW %HWND_DESKTOP, $AppName, , , 130, 110, _
           %WS_CAPTION OR %WS_MINIMIZEBOX OR %WS_SIZEBOX OR %WS_SYSMENU, %WS_EX_LEFT TO hDlg
          
           hIcon = ExtractIcon(GetModuleHandle(""), "Shell32.dll", 294) 'o
           SetClassLong(hDlg, %GCL_HICON, hIcon)
          
           DIM List(0 TO 9) AS STRING
           ARRAY ASSIGN List() = "Asynchronous is the way", "Do purge before speak", "zero", "one", "two", "three", "four", "five", "six", "seven"
           CONTROL ADD LISTBOX, hDlg, %Listbox01, List(), 5, 5, 120, 105, %LBS_NOTIFY OR %WS_VSCROLL, %WS_EX_LEFT OR %WS_EX_STATICEDGE
          
           DIALOG SHOW MODAL hDlg CALL DlgProc
          
           DestroyIcon(hIcon)
          
          END FUNCTION
          '_____________________________________________________________________________
          '

          Comment


          • #6
            Howdy, Pierre!

            Thanks, that definitely sounds better.

            When I do that, however, and when I quickly arrow key through five items, one or maybe two of the intervening items still speak.

            I suppose, that I could put a timer in place and only start speaking if there are more than XXms between speaking commands. I'd always stop speaking what a new item is selected, but not speak until at least XXms have passed.

            Comment


            • #7
              Instead of a timer, it might be easier to insert the pause in the string, aka ws1 = "<silence msec='200'/>" & ws1

              Comment


              • #8
                Pierre!

                So, it really is true, that you're more than just a pretty face? Excellent suggestion - I did not know that a pause could be inserted in the talking string.

                Yes, I did say "immediately", but I was perhaps overzealous with the comment.

                If a user uses the down arrow to move over the ListBox items, what would they want to hear? Say they quickly moved the selection through 5 items. Would they want to hear 5 quick, but brief, sounds? Or, would they just want to hear the last selection? I'm not sure of the answer to that just yet. Probably they would want to hear the last selection only, but that would lose the sense of "movement".

                If I put in the delay that you suggest, it might be best not to have it in the last selected item. But just modifying the string gives me no way to determine which is the last selected item. That would seem to require a timer plus the pause.

                I'll experiment with just the pause approach you suggest - varying the pause and trying out arrow movement/selection at different speeds to see if an obvious, pleasing/informative sound result becomes obvious.

                Comment


                • #9
                  Pierre!

                  More testing to be done, but first results suggest that somewhere around 300ms gives a balance between no intermediate sounds yet no obvious delay in sounding out the last selected position.

                  This will likely be one of those cases where each person might have a different preference, and where a user's PC might make a difference too.

                  Thanks again for pointing out the silence option! Very easy to implement.

                  Comment


                  • #10
                    Gary,
                    I don't hear any significant delay in starting to speak.
                    There is a delay at the end of a word because you're only speaking the words one at a time so they are treated more like paragraphs or sentences with a pause between each.
                    If you concatenate the words and speak them all together then there are no gaps and the words flow into each other.

                    It's also possible to speed up the speech by adjusting the rate.

                    Isn't the best solution to queue the words yourself and decide what needs to be spoken instead of trying to manipulate the speech interface to do it for you?
                    That way you can decide to join words so none are missed or skip intermediate words if you don't want them spoken when a subsequent choice is made which overrides the previous unspoken choice?


                    This code shows the speed being changed, a basic queuing method and flushing of previously unspoken words or sentences .. but only if they have not been started so it won't truncate words in progress.

                    Code:
                    'PBWin10 program
                    #COMPILE EXE
                    #DIM ALL
                    
                    %FlushSpeechQueue = 1
                    
                    FUNCTION PBMAIN () AS LONG
                    
                    sayit("Now is the time for all good men to come to the aid of the party.",0,0)     'the rate can vary from -10 (very slow) to +10 (very fast)
                    
                    MSGBOX "Click to do second line. If you OK this and the next message box before the first line is finished then the second line is dropped and not spoken."
                    
                    sayit("The quick brown fox jumps over the lazy dog.",5,0)     'the rate can vary from -10 (very slow) to +10 (very fast)
                    
                    MSGBOX "Click to do third line"
                    
                    sayit("Why did the chicken cross the road?",8,%FlushSpeechQueue)     'this will flush any speach that has not yet begun to bespoken
                    
                    
                    
                    
                    
                    
                    sayit("Speaking all the words in one sentence at rate 0",0,0)     'the rate can vary from -10 (very slow) to +10 (very fast)
                    sayit("Speaking",0,0)               'but when said as individual words it's much slower
                    sayit("at",0,0)
                    sayit("rate",0,0)
                    sayit("zero",0,0)
                    
                    
                    sayit("Speaking at rate 5",5,0)     'note that sending the whole string of words is spoken faster than ..
                    sayit("Speaking",5, 0)               '.. sending the words one at a time
                    sayit("at",5,0)
                    sayit("rate",5,0)
                    sayit("five",5,0)
                    
                    'sayit("Speaking at rate 8",8,%FlushSpeechQueue)        'REM one or other of these 2 lines to see that the previous unspoken words are flushed or not
                    sayit("Speaking at rate 8",8,0)                         'REM one or other of these 2 lines to see that the previous unspoken words are flushed or not
                    
                    sayit("Speaking",8,0)
                    sayit("at",8,0)
                    sayit("rate",8,0)
                    sayit("eight",8,0)
                    
                    MSGBOX "Click to end."
                    
                    END FUNCTION 'PBmain
                    
                    
                    
                    
                    
                    ' The following 2 functions and Type definition go together.
                    ' The intention is to queue requests so they all get said, even if delayed a little by the previous reguest not being finished.
                    ' No account is taken of the queue overflowing but it's currently set for 20 lines and if it's 20 lines behind then the data is well out of date anyway.
                    ' If the queue fills then the end index catches the start index and effectively deletes everything in the queue and starts over again.
                    
                    %MaxNoofSpeechLinesToQueue = 20
                    TYPE QspeechThreadTokenType
                    Qstart AS LONG
                    Qend AS LONG
                    LinesToSpeak(0 TO %MaxNoofSpeechLinesToQueue) AS STRING*1000
                    SpeedToSpeak(0 TO %MaxNoofSpeechLinesToQueue) AS LONG
                    
                    END TYPE
                    
                    
                    FUNCTION sayit(WhatToSay AS STRING, Rate AS LONG,  FlushQueue AS LONG) AS LONG
                    'the SayIt function was blocking the main code so call it in its own thread
                    'it must be done via a static as the new thread may not get access to the string until
                    ' after the string in changed by the main code.
                    
                    STATIC QspeechThreadToken AS QspeechThreadTokenType
                    
                    IF FlushQueue THEN
                    
                        'flush the queue
                        QspeechThreadToken.Qend = QspeechThreadToken.Qstart + 1
                        IF QspeechThreadToken.Qend > %MaxNoofSpeechLinesToQueue THEN QspeechThreadToken.Qend = 0
                    
                    END IF
                    
                    
                    'add the new line to the queue
                    QspeechThreadToken.LinesToSpeak(QspeechThreadToken.Qend) = WhatToSay
                    QspeechThreadToken.SpeedToSpeak(QspeechThreadToken.Qend) = Rate
                    
                    INCR QspeechThreadToken.Qend
                    IF QspeechThreadToken.Qend > %MaxNoofSpeechLinesToQueue THEN QspeechThreadToken.Qend = 0
                    
                    
                    LOCAL hThread AS LONG
                    
                        THREAD CREATE sayit1(VARPTR(QspeechThreadToken)) TO hThread
                        THREAD CLOSE hThread  TO hThread
                    
                    
                    END FUNCTION
                    
                    
                    
                    
                    THREAD FUNCTION sayit1(BYVAL ThePassedTokenAddress AS QspeechThreadTokenType PTR) AS LONG
                    'Do the actual speaking in a thread so the main program can continue with other stuff
                    'Read each line to speak from the queue and speak it.
                    ' When finished, if there are lines in the queue then immediatly speak them too
                    ' exit only when the queue is empty
                    
                    LOCAL QspeechThreadToken() AS QspeechThreadTokenType
                    STATIC ThreadAlreadyRunning AS LONG
                    
                    IF ThreadAlreadyRunning THEN EXIT FUNCTION  'speaking is in progress so it'll get around to speaking the last line withiout needing to re-run this thread.
                    
                    ThreadAlreadyRunning = 1
                    
                    DIM QspeechThreadToken(0) AT ThePassedTokenAddress
                    
                    WHILE QspeechThreadToken(0).Qstart <> QspeechThreadToken(0).Qend
                    
                        LOCAL oSp AS IDISPATCH
                    
                        LET oSp = NEWCOM "SAPI.SpVoice"
                        IF ISFALSE ISOBJECT(oSp) THEN EXIT FUNCTION
                    
                        LOCAL vRes AS VARIANT
                        LOCAL vTxt AS VARIANT
                        LOCAL vTime AS VARIANT
                    
                        LOCAL vRate AS VARIANT
                        LOCAL flags AS VARIANT
                    
                        vTxt = QspeechThreadToken(0).LinesToSpeak(QspeechThreadToken(0).Qstart)
                    
                        vRate = QspeechThreadToken(0).SpeedToSpeak(QspeechThreadToken(0).Qstart)
                        OBJECT LET oSp.Rate = vRate
                        OBJECT CALL oSp.Speak( vTxt,flags ) TO vRes
                    
                        vTime = -1 AS LONG
                    
                        OBJECT CALL oSp.WaitUntilDone(vTime) TO vRes
                    
                        IF ISTRUE ISOBJECT(oSp) THEN
                          SET oSp = NOTHING
                        END IF
                    
                    
                        INCR QspeechThreadToken(0).Qstart
                        IF QspeechThreadToken(0).Qstart > %MaxNoofSpeechLinesToQueue THEN QspeechThreadToken(0).Qstart = 0
                    
                    WEND
                    
                    ThreadAlreadyRunning = 0
                    
                    END FUNCTION

                    Comment


                    • #11
                      The same idea but integrated into your original app:
                      Code:
                      PBwin10 program
                      #COMPILE EXE
                      #DIM ALL
                      %Unicode=1
                      
                      #DEBUG ERROR ON
                      #DEBUG DISPLAY ON
                      
                      #INCLUDE "win32api.inc"
                      #INCLUDE "sapi.inc"
                      
                      %IDC_ListBox = 500
                      %Msg_SAPI_Event  = %WM_USER+1
                      
                      %FlushSpeechQueue = 1
                      
                      GLOBAL psp AS ISpVoice, TextToSpeak$$, hDlg,hThread AS DWORD
                      
                      FUNCTION PBMAIN() AS LONG
                      
                         DIM MyArray(7) AS STRING
                         ARRAY ASSIGN MyArray() = "zero", "one", "two", "three","four","five","six","seven"
                         DIALOG DEFAULT FONT "Tahoma", 21,1
                         DIALOG NEW PIXELS, 0, "ListBox Test",300,300,150,300, %WS_OVERLAPPEDWINDOW, 0 TO hDlg
                         CONTROL ADD LISTBOX, hDlg, %IDC_ListBox, MyArray(), 5,5,190,300
                         DIALOG SHOW MODAL hDlg CALL DlgProc
                      END FUNCTION
                      
                      CALLBACK FUNCTION DlgProc() AS LONG
                         LOCAL iRow AS LONG, temp$
                      
                         SELECT CASE CB.MSG
                            CASE %WM_INITDIALOG
                      '         pSp = NewCom "SAPI.SpVoice"
                      '         pSp.SetInterest(SPFEI(%SPEI_WORD_BOUNDARY) Or SPFEI(%SPEI_END_INPUT_STREAM), SPFEI(%SPEI_WORD_BOUNDARY) Or SPFEI(%SPEI_END_INPUT_STREAM))
                      '         pSp.SetNotifyWindowMessage(hDlg, %MSG_SAPI_EVENT, 0, 0)
                      
                            CASE %WM_COMMAND
                               SELECT CASE CB.CTL
                                  CASE %IDC_ListBox
                                     SELECT CASE CB.CTLMSG
                                        CASE %LBN_SELCHANGE
                                           LISTBOX GET SELECT hDlg, %IDC_ListBox TO iRow
                                           LISTBOX GET TEXT hDlg, %IDC_ListBox, iRow TO TextToSpeak$$
                      
                                           SayIt( TextToSpeak$$,0,%FlushSpeechQueue)    'the 0 is the speed. Increase to something between 1 and 10 to make it quicker
                      
                                         '  Thread Create SimpleReadText(0) To hThread
                                         '  Thread Close hThread To hThread
                                     END SELECT
                               END SELECT
                         END SELECT
                      END FUNCTION
                      
                      THREAD FUNCTION SimpleReadText(BYVAL X AS LONG) AS LONG
                         pSp.Speak(BYVAL STRPTR(TextToSpeak$$), %SPF_PurgeBeforeSpeak, BYVAL %Null)
                      END FUNCTION
                      
                      
                      
                      
                      
                      
                      
                      ' The following 2 functions and Type definition go together.
                      ' The intention is to queue requests so they all get said, even if delayed a little by the previous reguest not being finished.
                      ' No account is taken of the queue overflowing but it's currently set for 20 lines and if it's 20 lines behind then the data is well out of date anyway.
                      ' If the queue fills then the end index catches the start index and effectively deletes everything in the queue and starts over again.
                      
                      %MaxNoofSpeechLinesToQueue = 20
                      TYPE QspeechThreadTokenType
                      Qstart AS LONG
                      Qend AS LONG
                      LinesToSpeak(0 TO %MaxNoofSpeechLinesToQueue) AS STRING*1000
                      SpeedToSpeak(0 TO %MaxNoofSpeechLinesToQueue) AS LONG
                      
                      END TYPE
                      
                      
                      FUNCTION sayit(BYVAL WhatToSay AS STRING, Rate AS LONG,  FlushQueue AS LONG) AS LONG
                      'the SayIt function was blocking the main code so call it in its own thread
                      'it must be done via a static as the new thread may not get access to the string until
                      ' after the string in changed by the main code.
                      
                      STATIC QspeechThreadToken AS QspeechThreadTokenType
                      
                      
                      IF FlushQueue THEN
                      
                          'flush the queue
                          IF  ABS(QspeechThreadToken.Qend - QspeechThreadToken.Qstart) > 1 THEN
                              'if not yet speaking the last one, skip all the intermediate words
                              QspeechThreadToken.Qend = QspeechThreadToken.Qstart + 1
                      
                          END IF
                      
                          IF QspeechThreadToken.Qend > %MaxNoofSpeechLinesToQueue THEN QspeechThreadToken.Qend = 0
                      
                      END IF
                      
                      
                      'add the new line to the queue
                      QspeechThreadToken.LinesToSpeak(QspeechThreadToken.Qend) = WhatToSay
                      QspeechThreadToken.SpeedToSpeak(QspeechThreadToken.Qend) = Rate
                      
                      INCR QspeechThreadToken.Qend
                      IF QspeechThreadToken.Qend > %MaxNoofSpeechLinesToQueue THEN QspeechThreadToken.Qend = 0
                      
                      
                      LOCAL hThread AS LONG
                      
                          THREAD CREATE sayit1(VARPTR(QspeechThreadToken)) TO hThread
                          THREAD CLOSE hThread  TO hThread
                      
                      
                      END FUNCTION
                      
                      
                      
                      
                      THREAD FUNCTION sayit1(BYVAL ThePassedTokenAddress AS QspeechThreadTokenType PTR) AS LONG
                      'Do the actual speaking in a thread so the main program can continue with other stuff
                      'Read each line to speak from the queue and speak it.
                      ' When finished, if there are lines in the queue then immediatly speak them too
                      ' exit only when the queue is empty
                      
                      LOCAL QspeechThreadToken() AS QspeechThreadTokenType
                      STATIC ThreadAlreadyRunning AS LONG
                      
                      IF ThreadAlreadyRunning THEN EXIT FUNCTION  'speaking is in progress so it'll get around to speaking the last line withiout needing to re-run this thread.
                      
                      ThreadAlreadyRunning = 1
                      
                      DIM QspeechThreadToken(0) AT ThePassedTokenAddress
                      
                      WHILE QspeechThreadToken(0).Qstart <> QspeechThreadToken(0).Qend
                      
                          LOCAL oSp AS IDISPATCH
                      
                          LET oSp = NEWCOM "SAPI.SpVoice"
                          IF ISFALSE ISOBJECT(oSp) THEN EXIT FUNCTION
                      
                          LOCAL vRes AS VARIANT
                          LOCAL vTxt AS VARIANT
                          LOCAL vTime AS VARIANT
                      
                          LOCAL vRate AS VARIANT
                          LOCAL flags AS VARIANT
                      
                          vTxt = QspeechThreadToken(0).LinesToSpeak(QspeechThreadToken(0).Qstart)
                      
                          vRate = QspeechThreadToken(0).SpeedToSpeak(QspeechThreadToken(0).Qstart)
                          OBJECT LET oSp.Rate = vRate
                      
                          OBJECT CALL oSp.Speak( vTxt,flags ) TO vRes
                      
                          vTime = -1 AS LONG
                      
                          OBJECT CALL oSp.WaitUntilDone(vTime) TO vRes
                      
                          IF ISTRUE ISOBJECT(oSp) THEN
                            SET oSp = NOTHING
                          END IF
                      
                      
                          INCR QspeechThreadToken(0).Qstart
                          IF QspeechThreadToken(0).Qstart > %MaxNoofSpeechLinesToQueue THEN QspeechThreadToken(0).Qstart = 0
                      
                      WEND
                      
                      ThreadAlreadyRunning = 0
                      
                      END FUNCTION

                      Comment


                      • #12
                        Good morning, Paul, and thanks for the suggested code!

                        When I run the code of #11, and move steadily through the items using the arrow key through about 5 items, The items 1 and 5 are spoken, whereas items 2-4 are not spoken. Is that intended, how you envisioned it working? From reading your code, that seems to be a byproduct of the approach you took, not so much a choice (first and last spoken).

                        Comment


                        • #13
                          Paul,

                          Your suggestion is making me think!

                          I should mention that I'm interested in this type of interface for blind or vision-impaired users.

                          Isn't the best solution to queue the words yourself and decide what needs to be spoken ...
                          I think it would depend on the speed at which the user clicks an arrow key.

                          If the user moves slowly through items, then I'd want each to be spoken.

                          If the user moves quickly, then I would want only the last to be spoken. In that cases, the user's speed of movement would show no interest in hearing the intermediate items.

                          However, an abbreviated/short sound for each passed item would let the user hear how many items they are passing. In such a case, a simple/short audible tick/beep (not an interrupted item) would be enough to let the user track how far they've gone.

                          Wait - duh - they'll know because they are pressing a key and can just keep mental track of the number of times they've pressed the key. Still, a progress of ticks might be a useful secondary audible tracking mechanism.

                          So yes, the solution you suggest, with the constraints I mention, might be the best solution.

                          The simplicity of the "silence" tag is alluring. With the 300ms setting it comes close to allowing slow or jump-to-final results. But it does NOT offer intermediate sounds for tracking "distance" moved.

                          More fun to be had ...

                          Comment


                          • #14
                            When I run the code of #11, and move steadily through the items using the arrow key through about 5 items, The items 1 and 5 are spoken, whereas items 2-4 are not spoken.
                            Gary,
                            scrolling quickly down the list, yes that's what should happen because it'll speak the first value and then by the time that first value is spoken you'll be at the bottom of the list so it'll speak the last value.

                            But scrolling slowly down the list it should speak each value in turn.
                            At an intermediate speed it will only speak a value if speaking the previous one has finished so you might get "Five, Seven, Zero" .
                            If you want all values to be spoken, just replace the %FlushSpeechQueue with a zero.

                            Comment


                            • #15
                              I think it would depend on the speed at which the user clicks an arrow key.
                              That's why it's better to queue them yourself because you don't know how fast the user will move through the list and handling the queue yourself gives you the flexibility to cope with more situations.

                              If the user moves slowly through items, then I'd want each to be spoken.
                              That's what the code in post #11 does. Is that not what happens for you?

                              If the user moves quickly, then I would want only the last to be spoken.
                              That's what the code in post #11 does.

                              Comment


                              • #16
                                Gary, I typically use a Speech thread function with a FIFO buffer to hold the phrases to be voiced and then stop the voice like this:
                                Code:
                                SUB StopTheVoiceOutput
                                  LOCAL lwsType                AS WSTRINGZ * 20
                                  LOCAL liNumber               AS LONG
                                  local pulNumSkipped          AS DWORD
                                
                                  lwsType = "Sentence"
                                  liNumber = 60      'number of sentences to skip
                                
                                  ' "should extinguish local voice here."  
                                  IF ISFALSE ISOBJECT( oSp ) THEN
                                      ' "Speak Object not open"
                                  ELSE
                                      oSp.Skip(lwsType, liNumber, pulNumSkipped)  
                                  END IF
                                END SUB
                                The skip command only applies to what is currently being said and it has no effect on the FIFO buffer. The FIFO buffer is a nice feature since it can be loaded up with many phrases. The skip feature allows the user to skip through the voice data as they choose. The liNumber is arbitrary set high to jump out of a long sequence of phrases/sentences.

                                However, to do what you are doing does not need a FIFO buffer. The Skip will do the job quickly.

                                NOTE:
                                The oSp is global and stays alive for the life of the app.

                                Comment


                                • #17
                                  >But it does NOT offer intermediate sounds for tracking "distance" moved.
                                  Code:
                                   
                                  'Simply do
                                  WinBeep(2500, 30) 'Any sound that is not assync
                                  'before
                                  pSp.Speak("<silence msec='500'/>" & ws1, %SPF_ASYNC OR %SPF_PURGEBEFORESPEAK, BYVAL %NULL)

                                  Comment


                                  • #18
                                    You might also try multi-modal sound. Play a quick whisper (i.e. a brief sound not a spoken word) as users arrow through items so they get a sense of the number of items they're passing over, when they pause on an item, speak it. I'm not blind but I think that would work for me if I were. if you want to go all in you might consider haptic feedback as well. Not sure what you'd use, but I'm guessing there are devices already out there.

                                    Edit: Bleh I think that's essentially what Pierre just said.

                                    Also you don't need to use silence that you purge, just wait xx ms after a movement before issuing the speech. If the user starts moving while your speaking, overlay it with a quick silence (whatever the minimum is that will still purge) so you can start blipping over items. a blip with resetting a timer on move should work pretty well. And I guess pressing a key could be haptic feedback, but if they scrolled the mouse wheel for example it might be nice to know how many items were skipped over.
                                    LarryC
                                    Website
                                    Sometimes life's a dream, sometimes it's a scream

                                    Comment


                                    • #19
                                      Gary,
                                      Does this kludge work?
                                      I tried clearing mouse and keyboard buffer without success, but this worked well on my machine with quick up/down key presses.
                                      Code:
                                      FOR counter = 1 TO 4
                                         pSp.Speak(BYVAL STRPTR(TextToSpeak), %SPF_PurgeBeforeSpeak OR %SPF_ASYNC , BYVAL %Null)
                                       NEXT
                                      Code:
                                      #COMPILE EXE
                                      #DIM ALL
                                      %Unicode=1
                                      
                                      #DEBUG ERROR ON
                                      #DEBUG DISPLAY ON
                                      
                                      #INCLUDE "win32api.inc"
                                      #INCLUDE "sapi.inc"
                                      
                                      %IDC_ListBox = 500
                                      %Msg_SAPI_Event  = %WM_USER+1
                                      
                                      GLOBAL psp AS ISpVoice, TextToSpeak$$, hDlg,hThread AS DWORD
                                      FUNCTION PBMAIN() AS LONG
                                         DIM MyArray(7) AS STRING
                                         ARRAY ASSIGN MyArray() = "zero", "one", "two", "three","four","five","six","seven"
                                         DIALOG DEFAULT FONT "Tahoma", 21,1
                                         DIALOG NEW PIXELS, 0, "ListBox Test",300,300,150,300, %WS_OVERLAPPEDWINDOW, 0 TO hDlg
                                         CONTROL ADD LISTBOX, hDlg, %IDC_ListBox, MyArray(), 5,5,190,300
                                         DIALOG SHOW MODAL hDlg CALL DlgProc
                                      END FUNCTION
                                      
                                      CALLBACK FUNCTION DlgProc() AS LONG
                                         LOCAL iRow AS LONG, temp$
                                         SELECT CASE CB.MSG
                                            CASE %WM_INITDIALOG
                                               pSp = NEWCOM "SAPI.SpVoice"
                                               pSp.SetInterest(SPFEI(%SPEI_WORD_BOUNDARY) OR SPFEI(%SPEI_END_INPUT_STREAM), SPFEI(%SPEI_WORD_BOUNDARY) OR SPFEI(%SPEI_END_INPUT_STREAM))
                                               pSp.SetNotifyWindowMessage(hDlg, %MSG_SAPI_EVENT, 0, 0)
                                            CASE %WM_COMMAND
                                               SELECT CASE CB.CTL
                                                  CASE %IDC_ListBox
                                                     SELECT CASE CB.CTLMSG
                                                        CASE %LBN_SELCHANGE
                                                           LISTBOX GET SELECT hDlg, %IDC_ListBox TO iRow
                                                           LISTBOX GET TEXT hDlg, %IDC_ListBox, iRow TO TextToSpeak$$
                                                           THREAD CREATE SimpleReadText(0) TO hThread
                                                           THREAD CLOSE hThread TO hThread
                                                     END SELECT
                                               END SELECT
                                         END SELECT
                                      END FUNCTION
                                      
                                      THREAD FUNCTION SimpleReadText(BYVAL X AS LONG) AS LONG
                                        LOCAL counter AS LONG
                                        FOR counter = 1 TO 4
                                         pSp.Speak(BYVAL STRPTR(TextToSpeak), %SPF_PurgeBeforeSpeak OR %SPF_ASYNC , BYVAL %Null)
                                        NEXT
                                      END FUNCTION
                                      https://duckduckgo.com instead of google

                                      Comment


                                      • #20
                                        Sorry, guys - got distracted ...

                                        Paul,
                                        Yes, #11 does that.

                                        Mike!
                                        Yep, it works, pretty much the same as when I insert the silence that Pierre suggested. Two ways to skin the cat!

                                        Pierre/Larry,
                                        Yes, the insertion of WinBeep for a haptic sound is a very good idea. However, I'd want skip the WinBeep if the user just clicks on a single item

                                        Jim,
                                        Thanks for the skip suggestion. If I allow a voice to start, then skip, that should also provide a haptic-like response.

                                        Thanks everyone for the good suggestions.

                                        Comment

                                        Working...
                                        X