audio_player.go
package audio
import (
"bytes"
"sync"
"github.com/ebitengine/oto/v3"
)
// AudioPlayer wraps oto for simple audio playback.
// The oto context is created once and reused across plays.
type AudioPlayer struct {
ctx *oto.Context
mu sync.Mutex
player *oto.Player
}
// NewAudioPlayer creates a new audio player with the given sample rate.
// Only one AudioPlayer should exist per process (oto allows one context).
func NewAudioPlayer(sampleRate int) (*AudioPlayer, error) {
op := &oto.NewContextOptions{
SampleRate: sampleRate,
ChannelCount: 1,
Format: oto.FormatSignedInt16LE,
}
ctx, readyChan, err := oto.NewContext(op)
if err != nil {
return nil, err
}
<-readyChan
return &AudioPlayer{ctx: ctx}, nil
}
// Play stops any current playback and starts playing the given samples.
// Samples are float64 in the range -1.0 to 1.0.
// Playback is non-blocking — audio plays in the background.
func (ap *AudioPlayer) Play(samples []float64, sampleRate int) {
ap.PlayAtSpeed(samples, sampleRate, 1.0)
}
// PlayAtSpeed plays samples at the given speed (1.0 = normal, 0.5 = half speed).
// Speed change is achieved by resampling the audio.
// Playback is non-blocking — audio plays in the background.
func (ap *AudioPlayer) PlayAtSpeed(samples []float64, sampleRate int, speed float64) {
ap.mu.Lock()
defer ap.mu.Unlock()
// Stop previous playback
if ap.player != nil {
ap.player.Pause()
ap.player = nil
}
// Resample if speed is not normal
if speed != 1.0 {
samples = Resample(samples, speed)
}
// Convert float64 samples to signed int16 LE bytes
buf := Float64ToPCM16(samples)
ap.player = ap.ctx.NewPlayer(bytes.NewReader(buf))
ap.player.Play()
}
// IsPlaying returns true if audio is currently playing.
func (ap *AudioPlayer) IsPlaying() bool {
ap.mu.Lock()
defer ap.mu.Unlock()
return ap.player != nil && ap.player.IsPlaying()
}
// Stop stops any current playback.
func (ap *AudioPlayer) Stop() {
ap.mu.Lock()
defer ap.mu.Unlock()
if ap.player != nil {
ap.player.Pause()
ap.player = nil
}
}
// Close stops playback. The oto context is released by the garbage collector
// (oto v3 does not expose a context close method).