Files

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