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 WAV (RIFF/PCM) 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 := decodeWAV(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() }