212 lines
5.6 KiB
Go
212 lines
5.6 KiB
Go
// Package speaker implements playback of beep.Streamer values through physical speakers.
|
|
package speaker
|
|
|
|
import (
|
|
"io"
|
|
"sync"
|
|
"time"
|
|
|
|
"github.com/ebitengine/oto/v3"
|
|
"github.com/pkg/errors"
|
|
|
|
"github.com/gopxl/beep"
|
|
)
|
|
|
|
const channelCount = 2
|
|
const bitDepthInBytes = 2
|
|
const bytesPerSample = bitDepthInBytes * channelCount
|
|
const otoFormat = oto.FormatSignedInt16LE
|
|
|
|
var (
|
|
mu sync.Mutex
|
|
mixer beep.Mixer
|
|
context *oto.Context
|
|
player *oto.Player
|
|
|
|
bufferDuration time.Duration
|
|
)
|
|
|
|
// Init initializes audio playback through speaker. Must be called before using this package.
|
|
//
|
|
// The bufferSize argument specifies the number of samples of the speaker's buffer. Bigger
|
|
// bufferSize means lower CPU usage and more reliable playback. Lower bufferSize means better
|
|
// responsiveness and less delay.
|
|
func Init(sampleRate beep.SampleRate, bufferSize int) error {
|
|
if context != nil {
|
|
return errors.New("speaker cannot be initialized more than once")
|
|
}
|
|
|
|
mixer = beep.Mixer{}
|
|
|
|
// We split the total amount of buffer size between the driver and the player.
|
|
// This seems to be a decent ratio on my machine, but it may have different
|
|
// results on other OS's because of different underlying implementations.
|
|
// Both buffers try to keep themselves filled, so the total buffered
|
|
// number of samples should be some number less than bufferSize.
|
|
driverBufferSize := bufferSize / 2
|
|
playerBufferSize := bufferSize / 2
|
|
|
|
var err error
|
|
var readyChan chan struct{}
|
|
context, readyChan, err = oto.NewContext(&oto.NewContextOptions{
|
|
SampleRate: int(sampleRate),
|
|
ChannelCount: channelCount,
|
|
Format: otoFormat,
|
|
BufferSize: sampleRate.D(driverBufferSize),
|
|
})
|
|
if err != nil {
|
|
return errors.Wrap(err, "failed to initialize speaker")
|
|
}
|
|
<-readyChan
|
|
|
|
player = context.NewPlayer(newReaderFromStreamer(&mixer))
|
|
player.SetBufferSize(playerBufferSize * bytesPerSample)
|
|
player.Play()
|
|
|
|
bufferDuration = sampleRate.D(bufferSize)
|
|
|
|
return nil
|
|
}
|
|
|
|
// Close closes audio playback. However, the underlying driver context keeps existing, because
|
|
// closing it isn't supported (https://github.com/hajimehoshi/oto/issues/149). In most cases,
|
|
// there is certainly no need to call Close even when the program doesn't play anymore, because
|
|
// in properly set systems, the default mixer handles multiple concurrent processes.
|
|
func Close() {
|
|
if player != nil {
|
|
player.Close()
|
|
player = nil
|
|
Clear()
|
|
}
|
|
}
|
|
|
|
// Lock locks the speaker. While locked, speaker won't pull new data from the playing Streamers. Lock
|
|
// if you want to modify any currently playing Streamers to avoid race conditions.
|
|
//
|
|
// Always lock speaker for as little time as possible, to avoid playback glitches.
|
|
func Lock() {
|
|
mu.Lock()
|
|
}
|
|
|
|
// Unlock unlocks the speaker. Call after modifying any currently playing Streamer.
|
|
func Unlock() {
|
|
mu.Unlock()
|
|
}
|
|
|
|
// Play starts playing all provided Streamers through the speaker.
|
|
func Play(s ...beep.Streamer) {
|
|
mu.Lock()
|
|
mixer.Add(s...)
|
|
mu.Unlock()
|
|
}
|
|
|
|
// PlayAndWait plays all provided Streamers through the speaker and waits until they have all finished playing.
|
|
func PlayAndWait(s ...beep.Streamer) {
|
|
mu.Lock()
|
|
var wg sync.WaitGroup
|
|
wg.Add(len(s))
|
|
for _, e := range s {
|
|
mixer.Add(beep.Seq(e, beep.Callback(func() {
|
|
wg.Done()
|
|
})))
|
|
}
|
|
mu.Unlock()
|
|
|
|
// Wait for the streamers to drain.
|
|
wg.Wait()
|
|
|
|
// Wait the expected time it takes for the samples to reach the driver.
|
|
time.Sleep(bufferDuration)
|
|
}
|
|
|
|
// Suspend suspends the entire audio play.
|
|
//
|
|
// This function is intended to save resources when no audio is playing.
|
|
// To suspend individual streams, use the beep.Ctrl.
|
|
func Suspend() error {
|
|
err := context.Suspend()
|
|
if err != nil {
|
|
return errors.Wrap(err, "failed to suspend the speaker")
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// Resume resumes the entire audio play, which was suspended by Suspend.
|
|
func Resume() error {
|
|
err := context.Resume()
|
|
if err != nil {
|
|
return errors.Wrap(err, "failed to resume the speaker")
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// Clear removes all currently playing Streamers from the speaker.
|
|
// Previously buffered samples may still be played.
|
|
func Clear() {
|
|
mu.Lock()
|
|
mixer.Clear()
|
|
mu.Unlock()
|
|
}
|
|
|
|
// sampleReader is a wrapper for beep.Streamer to implement io.Reader.
|
|
type sampleReader struct {
|
|
s beep.Streamer
|
|
buf [][2]float64
|
|
}
|
|
|
|
func newReaderFromStreamer(s beep.Streamer) *sampleReader {
|
|
return &sampleReader{
|
|
s: s,
|
|
}
|
|
}
|
|
|
|
// Read pulls samples from the streamer and fills buf with the encoded
|
|
// samples. Read expects the size of buf be divisible by the length
|
|
// of a sample (= channel count * bit depth in bytes).
|
|
func (s *sampleReader) Read(buf []byte) (n int, err error) {
|
|
// Read samples from streamer
|
|
if len(buf)%bytesPerSample != 0 {
|
|
return 0, errors.New("requested number of bytes do not align with the samples")
|
|
}
|
|
ns := len(buf) / bytesPerSample
|
|
if len(s.buf) < ns {
|
|
s.buf = make([][2]float64, ns)
|
|
}
|
|
ns, ok := s.stream(s.buf[:ns])
|
|
if !ok {
|
|
if s.s.Err() != nil {
|
|
return 0, errors.Wrap(s.s.Err(), "streamer returned error when requesting samples")
|
|
}
|
|
if ns == 0 {
|
|
return 0, io.EOF
|
|
}
|
|
}
|
|
|
|
// Convert samples to bytes
|
|
for i := range s.buf[:ns] {
|
|
for c := range s.buf[i] {
|
|
val := s.buf[i][c]
|
|
if val < -1 {
|
|
val = -1
|
|
}
|
|
if val > +1 {
|
|
val = +1
|
|
}
|
|
valInt16 := int16(val * (1<<15 - 1))
|
|
low := byte(valInt16)
|
|
high := byte(valInt16 >> 8)
|
|
buf[i*bytesPerSample+c*bitDepthInBytes+0] = low
|
|
buf[i*bytesPerSample+c*bitDepthInBytes+1] = high
|
|
}
|
|
}
|
|
|
|
return ns * bytesPerSample, nil
|
|
}
|
|
|
|
// stream pull samples from the streamer while preventing concurrency
|
|
// problems by locking the global mixer.
|
|
func (s *sampleReader) stream(samples [][2]float64) (n int, ok bool) {
|
|
mu.Lock()
|
|
defer mu.Unlock()
|
|
return s.s.Stream(samples)
|
|
}
|