80 lines
2.2 KiB
Go
80 lines
2.2 KiB
Go
// Copyright (c) 2026 Storm Dragon
|
|
// Authors: Storm Dragon, Angus Kola
|
|
// SPDX-License-Identifier: GPL-3.0-or-later
|
|
|
|
package speech
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"time"
|
|
)
|
|
|
|
// SpeakAndWait uses SSIP BEGIN/END/CANCEL events, not the SPEAK queue
|
|
// acknowledgement, to pace continuous reading and report its current position.
|
|
// Protocol: https://github.com/brailcom/speechd/blob/master/doc/ssip.texi
|
|
func (client *Client) SpeakAndWait(ctx context.Context, text string, began func()) (result error) {
|
|
client.mutex.Lock()
|
|
defer client.mutex.Unlock()
|
|
if client.conn == nil {
|
|
return errors.New("speech client is not connected")
|
|
}
|
|
if err := client.sendCommand(ctx, []string{"SET", "SELF", "NOTIFICATION", "ALL", "on"}); err != nil {
|
|
return err
|
|
}
|
|
defer func() {
|
|
if client.conn != nil {
|
|
cleanup, cancel := context.WithTimeout(context.Background(), client.ioTimeout)
|
|
defer cancel()
|
|
if err := client.sendCommand(cleanup, []string{"SET", "SELF", "NOTIFICATION", "ALL", "off"}); err != nil {
|
|
result = errors.Join(result, err)
|
|
_ = client.conn.Close()
|
|
client.conn, client.reader = nil, nil
|
|
}
|
|
}
|
|
}()
|
|
if err := client.speak(ctx, text); err != nil {
|
|
return err
|
|
}
|
|
conn := client.conn
|
|
// A spoken line may take much longer than the command I/O timeout.
|
|
if err := conn.SetDeadline(time.Time{}); err != nil {
|
|
return err
|
|
}
|
|
done := make(chan struct{})
|
|
stop := context.AfterFunc(ctx, func() {
|
|
defer close(done)
|
|
cleanup, cancel := context.WithTimeout(context.Background(), client.ioTimeout)
|
|
defer cancel()
|
|
_ = client.Cancel(cleanup)
|
|
// Allow the final cancellation notification to be drained, preserving
|
|
// the speaking connection for subsequent focus announcements.
|
|
_ = conn.SetReadDeadline(time.Now().Add(client.ioTimeout))
|
|
})
|
|
defer func() {
|
|
if !stop() {
|
|
<-done
|
|
}
|
|
}()
|
|
started := false
|
|
for {
|
|
event, err := readReply(client.reader)
|
|
if err != nil {
|
|
_ = conn.Close()
|
|
client.conn, client.reader = nil, nil
|
|
return errors.Join(ctx.Err(), err)
|
|
}
|
|
switch event.code {
|
|
case 701:
|
|
if !started && ctx.Err() == nil && began != nil {
|
|
began()
|
|
}
|
|
started = true
|
|
case 702:
|
|
return ctx.Err()
|
|
case 703:
|
|
return context.Canceled
|
|
}
|
|
}
|
|
}
|