package audio import ( "context" "fmt" "io" ) // Sink names (part of the public contract; spec: audio.sink = "oto"|"null"). const ( NameOto = "oto" NameNull = "null" ) // 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). type Sink interface { // Play plays the data and blocks until the end or until ctx is // cancelled. Play(ctx context.Context, data []byte) error // Name returns the sink name (audio.sink configuration). Name() string } // 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. func NewSink(name string, log io.Writer) (Sink, error) { switch name { case NameOto: return OtoSink{}, nil case NameNull: return NullSink{W: log}, nil default: return nil, fmt.Errorf("unknown audio sink %q (allowed: %q, %q)", name, NameOto, NameNull) } } // 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. type NullSink struct { W io.Writer // optional log output; nil = discard silently } // Name implements Sink. func (NullSink) Name() string { return NameNull } // Play implements Sink: discards the data. func (n NullSink) Play(ctx context.Context, data []byte) error { if n.W != nil { fmt.Fprintf(n.W, "audio: %d bytes discarded (null sink)\n", len(data)) } return nil }