diff --git a/internal/audio/oto.go b/internal/audio/oto.go index c11f972..35647cc 100644 --- a/internal/audio/oto.go +++ b/internal/audio/oto.go @@ -91,7 +91,12 @@ func (OtoSink) Play(ctx context.Context, data []byte) error { player.Play() for player.IsPlaying() { if err := ctx.Err(); err != nil { - player.Close() + // Cancel: stop the sound NOW. oto's Close is a no-op that + // relies on the garbage collector's finalizer, so a cancelled + // announcement would play on to its end; Pause takes the + // player out of the mix immediately (only the device buffer, + // about 100 ms, drains). + player.Pause() return err } if err := player.Err(); err != nil { diff --git a/internal/provider/osbuiltin.go b/internal/provider/osbuiltin.go index b3e2450..6f1f03e 100644 --- a/internal/provider/osbuiltin.go +++ b/internal/provider/osbuiltin.go @@ -122,6 +122,12 @@ func (p OSBuiltin) SpeakDirect(ctx context.Context, text, lang string) error { } cmd := exec.CommandContext(ctx, "spd-say", "-w", "-l", lang, text) if out, err := cmd.CombinedOutput(); err != nil { + if ctx.Err() != nil { + // Cancelled: killing the waiting client does not stop + // speech-dispatcher's output; tell it to stop explicitly. + _ = exec.Command("spd-say", "-S").Run() + return ctx.Err() + } return fmt.Errorf("osbuiltin: spd-say failed: %w: %s", err, bytes.TrimSpace(out)) } return nil