From 538d42f9a61fabd92c659a5648e960a371d537b2 Mon Sep 17 00:00:00 2001 From: Christopher Cooper Date: Wed, 5 Jul 2017 18:10:03 -0400 Subject: [PATCH 1/4] add initial speaker implementation --- audio/example/sin/main.go | 67 ++++++++++++++++++++++ audio/speaker.go | 118 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 185 insertions(+) create mode 100644 audio/example/sin/main.go create mode 100644 audio/speaker.go diff --git a/audio/example/sin/main.go b/audio/example/sin/main.go new file mode 100644 index 0000000..c53a1a7 --- /dev/null +++ b/audio/example/sin/main.go @@ -0,0 +1,67 @@ +package main + +import ( + "math" + + "fmt" + + "os" + + "github.com/faiface/pixel/audio" +) + +type sine struct { + freq float64 + rate float64 + time float64 +} + +func (s *sine) Stream(samples [][2]float64) (n int, ok bool) { + if len(samples) == 0 { + os.Exit(-1) + } + fmt.Println(len(samples)) + for i := 0; i < len(samples)-2; i += 2 { + val := math.Sin(math.Pi*s.time*s.freq) / 1.1 + s.time += 1 / s.rate + valI := int16((1 << 15) * val) + low := float64(valI % (1 << 8)) + high := float64(valI / (1 << 8)) + samples[i][0] = low + samples[i][1] = high + samples[i+1][0] = low + samples[i+1][1] = high + } + fmt.Println(samples) + return len(samples), true +} + +func main() { + err := run() + if err != nil { + fmt.Println(err) + } +} + +func run() error { + audio.SampleRate = 44100 + const bufSize = 1 << 13 + + speaker, err := audio.NewDefaultSpeaker(bufSize) + if err != nil { + return err + } + + s := &sine{freq: 440, rate: audio.SampleRate, time: 0} + + speaker.Play(s) + + for { + err := speaker.Update() + if err != nil { + return err + } + } + + return nil +} diff --git a/audio/speaker.go b/audio/speaker.go new file mode 100644 index 0000000..628d6a4 --- /dev/null +++ b/audio/speaker.go @@ -0,0 +1,118 @@ +package audio + +import ( + "errors" + + "io" + + "github.com/hajimehoshi/oto" +) + +type Speaker interface { + Play(Streamer) + Update() error +} + +type DefaultSpeaker struct { + Streamer + isPlaying bool + samples [][2]float64 + player *oto.Player + buf []uint8 + bufferSize int +} + +var ( + ErrBufferMustBePowerOf2 = errors.New("Buffer passed to Read must be a power of 2") +) + +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 +} + +func (s *DefaultSpeaker) eof() bool { + return len(s.samples) == 0 +} + +type Sample [2]float64 + +func (s *DefaultSpeaker) readSample() Sample { + sample := s.samples[0] + s.samples = s.samples[1:] + return sample +} + +func NewDefaultSpeaker(bufferSize int) (*DefaultSpeaker, error) { + p, err := oto.NewPlayer(int(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 +} + +func (bs *DefaultSpeaker) Play(s Streamer) { + bs.isPlaying = true + bs.Streamer = s +} + +func (b *DefaultSpeaker) Update() error { + + if b.isPlaying { + n, ok := b.Stream(b.samples) + if n == len(b.samples) && ok { + r, err := b.Read(b.buf) + if err != nil { + return err + } + b.buf = b.buf[:r] + _, err = b.player.Write(b.buf) + if err != nil { + return err + } + b.buf = make([]byte, b.bufferSize) + } + // we're read bytes but drained the streamer, so copy data and stop playing + if n > 0 && n < len(b.samples) && ok { + r, err := b.Read(b.buf) + if err != nil { + return err + } + b.buf = b.buf[:r] + _, err = b.player.Write(b.buf) + if err != nil { + return err + } + b.isPlaying = false + } + // this stream is already drained, set isPlaying to false + if n == 0 && !ok { + b.isPlaying = false + } + } + return nil +} From 6dd3a9a64381390808bb9b91b46500a66f810d9e Mon Sep 17 00:00:00 2001 From: Christopher Cooper Date: Wed, 5 Jul 2017 18:11:26 -0400 Subject: [PATCH 2/4] mremove prints --- audio/example/sin/main.go | 2 -- 1 file changed, 2 deletions(-) diff --git a/audio/example/sin/main.go b/audio/example/sin/main.go index c53a1a7..881f9b9 100644 --- a/audio/example/sin/main.go +++ b/audio/example/sin/main.go @@ -20,7 +20,6 @@ func (s *sine) Stream(samples [][2]float64) (n int, ok bool) { if len(samples) == 0 { os.Exit(-1) } - fmt.Println(len(samples)) for i := 0; i < len(samples)-2; i += 2 { val := math.Sin(math.Pi*s.time*s.freq) / 1.1 s.time += 1 / s.rate @@ -32,7 +31,6 @@ func (s *sine) Stream(samples [][2]float64) (n int, ok bool) { samples[i+1][0] = low samples[i+1][1] = high } - fmt.Println(samples) return len(samples), true } From f542f75202089c9a168da309bf314fd42dcf5d1c Mon Sep 17 00:00:00 2001 From: Christopher Cooper Date: Wed, 5 Jul 2017 18:15:53 -0400 Subject: [PATCH 3/4] fix bounds check --- audio/example/sin/main.go | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/audio/example/sin/main.go b/audio/example/sin/main.go index 881f9b9..e018294 100644 --- a/audio/example/sin/main.go +++ b/audio/example/sin/main.go @@ -5,8 +5,6 @@ import ( "fmt" - "os" - "github.com/faiface/pixel/audio" ) @@ -17,10 +15,7 @@ type sine struct { } func (s *sine) Stream(samples [][2]float64) (n int, ok bool) { - if len(samples) == 0 { - os.Exit(-1) - } - for i := 0; i < len(samples)-2; i += 2 { + for i := 0; i < len(samples)-1; i += 2 { val := math.Sin(math.Pi*s.time*s.freq) / 1.1 s.time += 1 / s.rate valI := int16((1 << 15) * val) From 00cab859c19c1829c3600f3761fd5f756e55fafc Mon Sep 17 00:00:00 2001 From: Christopher Cooper Date: Thu, 6 Jul 2017 11:49:35 -0400 Subject: [PATCH 4/4] addresses some review comments --- audio/example/sin/main.go | 60 ---------------- audio/playback/speaker.go | 139 ++++++++++++++++++++++++++++++++++++++ audio/speaker.go | 118 -------------------------------- 3 files changed, 139 insertions(+), 178 deletions(-) delete mode 100644 audio/example/sin/main.go create mode 100644 audio/playback/speaker.go delete mode 100644 audio/speaker.go diff --git a/audio/example/sin/main.go b/audio/example/sin/main.go deleted file mode 100644 index e018294..0000000 --- a/audio/example/sin/main.go +++ /dev/null @@ -1,60 +0,0 @@ -package main - -import ( - "math" - - "fmt" - - "github.com/faiface/pixel/audio" -) - -type sine struct { - freq float64 - rate float64 - time float64 -} - -func (s *sine) Stream(samples [][2]float64) (n int, ok bool) { - for i := 0; i < len(samples)-1; i += 2 { - val := math.Sin(math.Pi*s.time*s.freq) / 1.1 - s.time += 1 / s.rate - valI := int16((1 << 15) * val) - low := float64(valI % (1 << 8)) - high := float64(valI / (1 << 8)) - samples[i][0] = low - samples[i][1] = high - samples[i+1][0] = low - samples[i+1][1] = high - } - return len(samples), true -} - -func main() { - err := run() - if err != nil { - fmt.Println(err) - } -} - -func run() error { - audio.SampleRate = 44100 - const bufSize = 1 << 13 - - speaker, err := audio.NewDefaultSpeaker(bufSize) - if err != nil { - return err - } - - s := &sine{freq: 440, rate: audio.SampleRate, time: 0} - - speaker.Play(s) - - for { - err := speaker.Update() - if err != nil { - return err - } - } - - return nil -} diff --git a/audio/playback/speaker.go b/audio/playback/speaker.go new file mode 100644 index 0000000..465c810 --- /dev/null +++ b/audio/playback/speaker.go @@ -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 +} diff --git a/audio/speaker.go b/audio/speaker.go deleted file mode 100644 index 628d6a4..0000000 --- a/audio/speaker.go +++ /dev/null @@ -1,118 +0,0 @@ -package audio - -import ( - "errors" - - "io" - - "github.com/hajimehoshi/oto" -) - -type Speaker interface { - Play(Streamer) - Update() error -} - -type DefaultSpeaker struct { - Streamer - isPlaying bool - samples [][2]float64 - player *oto.Player - buf []uint8 - bufferSize int -} - -var ( - ErrBufferMustBePowerOf2 = errors.New("Buffer passed to Read must be a power of 2") -) - -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 -} - -func (s *DefaultSpeaker) eof() bool { - return len(s.samples) == 0 -} - -type Sample [2]float64 - -func (s *DefaultSpeaker) readSample() Sample { - sample := s.samples[0] - s.samples = s.samples[1:] - return sample -} - -func NewDefaultSpeaker(bufferSize int) (*DefaultSpeaker, error) { - p, err := oto.NewPlayer(int(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 -} - -func (bs *DefaultSpeaker) Play(s Streamer) { - bs.isPlaying = true - bs.Streamer = s -} - -func (b *DefaultSpeaker) Update() error { - - if b.isPlaying { - n, ok := b.Stream(b.samples) - if n == len(b.samples) && ok { - r, err := b.Read(b.buf) - if err != nil { - return err - } - b.buf = b.buf[:r] - _, err = b.player.Write(b.buf) - if err != nil { - return err - } - b.buf = make([]byte, b.bufferSize) - } - // we're read bytes but drained the streamer, so copy data and stop playing - if n > 0 && n < len(b.samples) && ok { - r, err := b.Read(b.buf) - if err != nil { - return err - } - b.buf = b.buf[:r] - _, err = b.player.Write(b.buf) - if err != nil { - return err - } - b.isPlaying = false - } - // this stream is already drained, set isPlaying to false - if n == 0 && !ok { - b.isPlaying = false - } - } - return nil -}