Files
talk/internal/audio/oto.go
T

106 lines
2.7 KiB
Go
Raw Normal View History

package audio
import (
"bytes"
"context"
"fmt"
"sync"
"time"
"github.com/ebitengine/oto/v3"
)
// OtoSink spielt Audio embedded über oto (github.com/ebitengine/oto/v3,
// gepinnt auf v3.5.0) ab — kein externer Player nötig. oto lädt die
// Audiotreiber zur Laufzeit ohne CGO (purego), dadurch bleibt talk
// cross-compilierbar.
type OtoSink struct{}
// Name implementiert Sink.
func (OtoSink) Name() string { return NameOto }
// contextCache hält je PCM-Format einen oto-Kontext vor: oto unterstützt
// nur EINEN Kontext pro Prozess ("Creating multiple contexts is NOT
// supported"), und ein Kontext ist an genau ein Format gebunden. Die CLI
// spielt pro Prozess einmal (Task 02); der Daemon (Task 04) profitiert vom
// Cache.
var contextCache = struct {
sync.Mutex
byFormat map[pcmFormat]*otoContext
}{byFormat: map[pcmFormat]*otoContext{}}
type otoContext struct {
ctx *oto.Context
ready chan struct{}
}
// otoFormat mappt ein PCM-Format auf das oto-Format.
func (f pcmFormat) otoFormat() oto.Format {
switch {
case f.Float:
return oto.FormatFloat32LE
case f.BitsPerCoded == 8:
return oto.FormatUnsignedInt8
default:
return oto.FormatSignedInt16LE
}
}
// contextFor liefert den (gecachten) oto-Kontext für ein Format.
func contextFor(f pcmFormat) (*otoContext, error) {
contextCache.Lock()
defer contextCache.Unlock()
if c, ok := contextCache.byFormat[f]; ok {
return c, nil
}
op := &oto.NewContextOptions{
SampleRate: f.SampleRate,
ChannelCount: f.Channels,
Format: f.otoFormat(),
}
ctx, ready, err := oto.NewContext(op)
if err != nil {
return nil, fmt.Errorf("oto: Kontext (Audio-Gerät?) fehlgeschlagen: %w", err)
}
c := &otoContext{ctx: ctx, ready: ready}
contextCache.byFormat[f] = c
return c, nil
}
// Play implementiert Sink: dekodiert die Daten (RIFF/WAVE-PCM von osbuiltin
// oder MP3 von den Cloud-Providern, siehe mp3.go) und spielt die Samples
// blockierend ab. Fehler des Audio-Geräts oder des Formats werden klar
// gemeldet.
func (OtoSink) Play(ctx context.Context, data []byte) error {
f, pcm, err := decodeAudio(data)
if err != nil {
return err
}
c, err := contextFor(f)
if err != nil {
return err
}
select {
case <-c.ready:
if err := c.ctx.Err(); err != nil {
return fmt.Errorf("oto: Initialisierung fehlgeschlagen: %w", err)
}
case <-ctx.Done():
return ctx.Err()
}
player := c.ctx.NewPlayer(bytes.NewReader(pcm))
defer player.Close()
player.Play()
for player.IsPlaying() {
if err := ctx.Err(); err != nil {
player.Close()
return err
}
if err := player.Err(); err != nil {
return fmt.Errorf("oto: Wiedergabe fehlgeschlagen: %w", err)
}
time.Sleep(5 * time.Millisecond)
}
return player.Err()
}