Merge pull request #44 from alistanis/audio

initial incomplete speaker implementation
This commit is contained in:
Michal Štrba 2017-07-06 20:52:02 +02:00 committed by GitHub
commit 7518c708d3
1 changed files with 139 additions and 0 deletions

139
audio/playback/speaker.go Normal file
View File

@ -0,0 +1,139 @@
package playback
import (
"errors"
"io"
"github.com/faiface/pixel/audio"
"github.com/hajimehoshi/oto"
)
// Speaker is the interface used for playing back audio.Streamers.
type Speaker interface {
// Play tells the Speaker that it is ready for playback and handles preparing the Streamer.
Play(audio.Streamer)
// Update is called once per game loop and handles pulling samples from the Streamer and writing them to Speaker's
// player.
Update() error
}
// DefaultSpeaker is a default implementation of speaker capable of playing back samples to the default output device.
type DefaultSpeaker struct {
// audio.Streamer is the Streamer to pull samples from. It is passed in and set with Speaker.Play(audio.Streamer)
audio.Streamer
// isPlaying informs the update loop about whether or not this Speaker is playing
isPlaying bool
// samples is the internal buffer of samples that read() and readSample() fill and drain, respectively
// samples' length is the total buffer size / 2
samples [][2]float64
// player is the underlying *oto.Player, which uses os specific APIs for audio playback
player *oto.Player
// buf is the buffer of samples converted to bytes that is written to player
buf []uint8
// bufferSize is the size in bytes of the total buffer in bytes. bufferSize must be a power of 2.
bufferSize int
}
// NewDefaultSpeaker returns a *DefaultSpeaker ready to read samples and write to the underlying player for playback
func NewDefaultSpeaker(bufferSize int) (*DefaultSpeaker, error) {
p, err := oto.NewPlayer(int(audio.SampleRate), 2, 2, bufferSize)
if err != nil {
return nil, err
}
return &DefaultSpeaker{
player: p,
samples: make([][2]float64, bufferSize/2),
buf: make([]uint8, bufferSize),
bufferSize: bufferSize,
}, nil
}
var (
// ErrBufferMustBePowerOf2 should be returned when the buffer passed in to read is not a power of 2, as a sample is
// 2 bytes, the buffer must have the capacity to handle all samples.
ErrBufferMustBePowerOf2 = errors.New("Buffer passed to Read must be a power of 2")
)
// read reads up to len(dst) / 2 samples into dst
func (s *DefaultSpeaker) read(dst []byte) (n int, err error) {
if !s.isPlaying || s.eof() {
return 0, io.EOF
}
// we need dst to be a power of two in order for us to write samples cleanly
if len(dst)%2 != 0 {
return 0, ErrBufferMustBePowerOf2
}
if l := len(dst); l > 1 {
for n < l-1 {
sample := s.readSample()
dst[n] = byte(sample[0])
dst[n+1] = byte(sample[1])
if s.eof() {
s.samples = make([][2]float64, s.bufferSize/2)
break
}
n += 2
}
}
return n, nil
}
// eof returns whether or not we have read all samples currently in the samples buffer
func (s *DefaultSpeaker) eof() bool {
return len(s.samples) == 0
}
// Sample is a single sample stored as an array of [2]float64, with Sample[0] being the left channel and Sample[1] being the right channel
type Sample [2]float64
// readSample reads a single sample from s.samples and truncates it from the buffer
func (s *DefaultSpeaker) readSample() Sample {
sample := s.samples[0]
s.samples = s.samples[1:]
return sample
}
// Play initializes the Streamer and sets s.isPlaying to true
func (ds *DefaultSpeaker) Play(s audio.Streamer) {
ds.isPlaying = true
ds.Streamer = s
}
// streamToPlayer Streams up to len(s.samples) into s.samples, converts those into bytes for s.buf, and writes s.buf
// to the underlying player
func (s *DefaultSpeaker) streamToPlayer() error {
n, ok := s.Stream(s.samples)
if (n == len(s.samples) || 0 < n && n < len(s.samples)) && ok {
r, err := s.read(s.buf)
if err != nil {
return err
}
s.buf = s.buf[:r]
_, err = s.player.Write(s.buf)
if err != nil {
return err
}
// we drained the streamer while while reading,
if n < len(s.samples) {
s.isPlaying = false
return nil
}
s.buf = make([]byte, s.bufferSize)
}
// this stream is already drained, set isPlaying to false
if n == 0 && !ok {
s.isPlaying = false
}
return nil
}
// Update should be called during the main update loop in order to handle synchronization
// If s.isPlaying, Update will stream all available samples to the underlying player once per update.
func (s *DefaultSpeaker) Update() error {
if s.isPlaying {
return s.streamToPlayer()
}
return nil
}