2026-10-18 15:39:09 +02:00
|
|
|
package audio
|
|
|
|
|
|
|
|
|
|
import (
|
|
|
|
|
"context"
|
|
|
|
|
"fmt"
|
|
|
|
|
"io"
|
|
|
|
|
)
|
|
|
|
|
|
2026-10-24 11:15:35 +02:00
|
|
|
// Sink names (part of the public contract; spec: audio.sink = "oto"|"null").
|
2026-10-18 15:39:09 +02:00
|
|
|
const (
|
|
|
|
|
NameOto = "oto"
|
|
|
|
|
NameNull = "null"
|
|
|
|
|
)
|
|
|
|
|
|
2026-10-24 11:15:35 +02:00
|
|
|
// Sink plays audio bytes. The bytes come from Provider.Synthesize; oto
|
|
|
|
|
// understands RIFF/WAVE (PCM, the delivery format of osbuiltin) and MP3
|
|
|
|
|
// (the delivery format of the cloud providers ElevenLabs/OpenAI/Edge).
|
2026-10-18 15:39:09 +02:00
|
|
|
type Sink interface {
|
2026-10-24 11:15:35 +02:00
|
|
|
// Play plays the data and blocks until the end or until ctx is
|
|
|
|
|
// cancelled.
|
2026-10-18 15:39:09 +02:00
|
|
|
Play(ctx context.Context, data []byte) error
|
|
|
|
|
|
2026-10-24 11:15:35 +02:00
|
|
|
// Name returns the sink name (audio.sink configuration).
|
2026-10-18 15:39:09 +02:00
|
|
|
Name() string
|
|
|
|
|
}
|
|
|
|
|
|
2026-10-24 11:15:35 +02:00
|
|
|
// NewSink returns the sink for the configured name ("oto" or "null"). log
|
|
|
|
|
// is an optional log output: the CLI passes its stderr so the null sink can
|
|
|
|
|
// write its discard line (nil = discard silently); the oto sink needs no
|
|
|
|
|
// log. Unknown names are a clear error.
|
2026-10-18 15:46:20 +02:00
|
|
|
func NewSink(name string, log io.Writer) (Sink, error) {
|
2026-10-18 15:39:09 +02:00
|
|
|
switch name {
|
|
|
|
|
case NameOto:
|
|
|
|
|
return OtoSink{}, nil
|
|
|
|
|
case NameNull:
|
2026-10-18 15:46:20 +02:00
|
|
|
return NullSink{W: log}, nil
|
2026-10-18 15:39:09 +02:00
|
|
|
default:
|
2026-10-24 11:15:35 +02:00
|
|
|
return nil, fmt.Errorf("unknown audio sink %q (allowed: %q, %q)", name, NameOto, NameNull)
|
2026-10-18 15:39:09 +02:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-10-24 11:15:35 +02:00
|
|
|
// NullSink discards every announcement without sound; for CI, servers and
|
|
|
|
|
// tests. If W is set (NewSink passes the CLI's log output), Play logs one
|
|
|
|
|
// line about it so "runs through silently" stays visible.
|
2026-10-18 15:39:09 +02:00
|
|
|
type NullSink struct {
|
2026-10-24 11:15:35 +02:00
|
|
|
W io.Writer // optional log output; nil = discard silently
|
2026-10-18 15:39:09 +02:00
|
|
|
}
|
|
|
|
|
|
2026-10-24 11:15:35 +02:00
|
|
|
// Name implements Sink.
|
2026-10-18 15:39:09 +02:00
|
|
|
func (NullSink) Name() string { return NameNull }
|
|
|
|
|
|
2026-10-24 11:15:35 +02:00
|
|
|
// Play implements Sink: discards the data.
|
2026-10-18 15:39:09 +02:00
|
|
|
func (n NullSink) Play(ctx context.Context, data []byte) error {
|
|
|
|
|
if n.W != nil {
|
2026-10-24 11:15:35 +02:00
|
|
|
fmt.Fprintf(n.W, "audio: %d bytes discarded (null sink)\n", len(data))
|
2026-10-18 15:39:09 +02:00
|
|
|
}
|
|
|
|
|
return nil
|
|
|
|
|
}
|