subsonic-tui/vendor/github.com/gopxl/beep/speaker/speaker.go
Sagi Dayan a3923cf42c initial commit
Signed-off-by: Sagi Dayan <sagidayan@gmail.com>
2024-03-29 17:56:39 +03:00

213 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)
}