oto-Sink bekommt eine Format-Weiche: RIFF/WAVE-PCM (osbuiltin) und MP3 (ElevenLabs/OpenAI/Edge ab Task 03). Dekoder ist github.com/hajimehoshi/ go-mp3 v0.3.4 — reines Go ohne CGO, verifiziert per GOOS/GOARCH-Matrix mit CGO_ENABLED=0 (alle 6 Targets bauen). Der Dekoder liefert immer 16-Bit-LE stereo (Mono-Quellen wie Edge 24 kHz werden gespiegelt); die Sample-Rate kommt aus dem ersten Frame. Fixtures sind echte, einmalig erzeugte MP3s (mono 24 kHz 48 kbit/s = Edge-Format, stereo 44,1 kHz = Cloud-typisch), als Base64 eingebettet — Tests bleiben werkzeugfrei.
59 lines
1.8 KiB
Go
59 lines
1.8 KiB
Go
package audio
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"io"
|
|
)
|
|
|
|
// Sink-Namen (Teil des öffentlichen Vertrags; Spec: audio.sink = "oto"|"null").
|
|
const (
|
|
NameOto = "oto"
|
|
NameNull = "null"
|
|
)
|
|
|
|
// Sink gibt Audio-Bytes wieder. Die Bytes stammen aus Provider.Synthesize;
|
|
// oto versteht RIFF/WAVE (PCM — das Lieferformat von osbuiltin) und MP3
|
|
// (das Lieferformat der Cloud-Provider ElevenLabs/OpenAI/Edge).
|
|
type Sink interface {
|
|
// Play spielt die Daten blockierend ab (bis zum Ende oder bis ctx
|
|
// abgebrochen wird).
|
|
Play(ctx context.Context, data []byte) error
|
|
|
|
// Name liefert den Sink-Namen (audio.sink-Konfiguration).
|
|
Name() string
|
|
}
|
|
|
|
// NewSink liefert den Sink zum konfigurierten Namen ("oto" oder "null").
|
|
// log ist ein optionaler Log-Output — die CLI übergibt ihr stderr, über den
|
|
// der null-Sink seine Verwurf-Zeile schreibt (nil = still verwerfen); der
|
|
// oto-Sink braucht kein Log. Unbekannte Namen sind ein klarer Fehler.
|
|
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("unbekannter audio sink %q (erlaubt: %q, %q)", name, NameOto, NameNull)
|
|
}
|
|
}
|
|
|
|
// NullSink verwirft jede Ansage ohne Ton — für CI, Server und Tests. Wurde
|
|
// ein W gesetzt (NewSink reicht dafür den Log-Output der CLI durch), loggt
|
|
// Play eine Zeile darüber, damit „läuft ohne Ton durch" sichtbar bleibt.
|
|
type NullSink struct {
|
|
W io.Writer // optionaler Log-Output; nil = still verwerfen
|
|
}
|
|
|
|
// Name implementiert Sink.
|
|
func (NullSink) Name() string { return NameNull }
|
|
|
|
// Play implementiert Sink: verwirft die Daten.
|
|
func (n NullSink) Play(ctx context.Context, data []byte) error {
|
|
if n.W != nil {
|
|
fmt.Fprintf(n.W, "audio: %d Bytes verworfen (null sink)\n", len(data))
|
|
}
|
|
return nil
|
|
}
|