From f0d7a811e1bd31eccde8b5045a1207534b5616c5 Mon Sep 17 00:00:00 2001 From: Hajime Hoshi Date: Fri, 12 Feb 2016 03:06:04 +0900 Subject: [PATCH] audio: Enable to play inifinite stream --- exp/audio/audio.go | 6 +- exp/audio/audio_openal.go | 119 ++++++++++++++++++++++++++++++++++---- 2 files changed, 110 insertions(+), 15 deletions(-) diff --git a/exp/audio/audio.go b/exp/audio/audio.go index 54d862d95..32bd420fa 100644 --- a/exp/audio/audio.go +++ b/exp/audio/audio.go @@ -19,7 +19,7 @@ import ( ) type Player struct { - *player + player *player } // NewPlayer creates a new player with the given data to the given channel. @@ -35,9 +35,9 @@ func NewPlayer(src io.ReadSeeker, sampleRate int) *Player { } func (p *Player) Play() error { - return p.play() + return p.player.play() } func (p *Player) Close() error { - return p.close() + return p.player.close() } diff --git a/exp/audio/audio_openal.go b/exp/audio/audio_openal.go index 572d4c418..338e8539e 100644 --- a/exp/audio/audio_openal.go +++ b/exp/audio/audio_openal.go @@ -17,9 +17,13 @@ package audio import ( + "fmt" "io" + "runtime" + "sync" + "time" - "golang.org/x/mobile/exp/audio" + "golang.org/x/mobile/exp/audio/al" ) type readSeekCloser struct { @@ -31,27 +35,118 @@ func (r *readSeekCloser) Close() error { } type player struct { - *audio.Player + alSource al.Source + alBuffers []al.Buffer + source io.ReadSeeker + sampleRate int } +var m sync.Mutex + +var sn = 0 + func newPlayer(src io.ReadSeeker, sampleRate int) *Player { - p, err := audio.NewPlayer(&readSeekCloser{src}, audio.Stereo16, int64(sampleRate)) - if err != nil { - // TODO: Should we return errors for this method? - panic(err) + m.Lock() + if err := al.OpenDevice(); err != nil { + panic(fmt.Sprintf("audio: OpenAL initialization failed: %v", err)) } - pp := &player{p} - return &Player{pp} + // TODO: Too many generating sources may cause error. Limit the number of sources. + s := al.GenSources(1) + if err := al.Error(); err != 0 { + panic(fmt.Sprintf("audio: al.GenSources error: %d", err)) + } + m.Unlock() + p := &player{ + alSource: s[0], + alBuffers: []al.Buffer{}, + source: src, + sampleRate: sampleRate, + } + runtime.SetFinalizer(p, (*player).close) + return &Player{p} +} + +const bufferSize = 1024 + +func (p *player) proceed() error { + m.Lock() + processedNum := p.alSource.BuffersProcessed() + if 0 < processedNum { + bufs := make([]al.Buffer, processedNum) + p.alSource.UnqueueBuffers(bufs...) + p.alBuffers = append(p.alBuffers, bufs...) + } + m.Unlock() + for 0 < len(p.alBuffers) { + b := make([]byte, bufferSize) + n, err := p.source.Read(b) + if 0 < n { + m.Lock() + buf := p.alBuffers[0] + p.alBuffers = p.alBuffers[1:] + buf.BufferData(al.FormatStereo16, b[:n], int32(p.sampleRate)) + p.alSource.QueueBuffers(buf) + m.Unlock() + } + if err != nil { + return err + } + } + m.Lock() + if p.alSource.State() == al.Stopped { + al.RewindSources(p.alSource) + al.PlaySources(p.alSource) + } + m.Unlock() + + return nil } func (p *player) play() error { - // TODO: audio.NewPlayer interprets WAV header, which we don't want. - // Use OpenAL or native API instead. - return p.Play() + // TODO: What if play is already called? + emptyBytes := make([]byte, bufferSize) + m.Lock() + bufs := al.GenBuffers(8) + for _, buf := range bufs { + // Note that the third argument of only the first buffer is used. + buf.BufferData(al.FormatStereo16, emptyBytes, int32(p.sampleRate)) + p.alSource.QueueBuffers(buf) + } + al.PlaySources(p.alSource) + m.Unlock() + go func() { + defer p.close() + for { + err := p.proceed() + if err == io.EOF { + break + } + if err != nil { + // TODO: Record the last error + panic(err) + } + time.Sleep(1) + } + }() + return nil } func (p *player) close() error { - return p.Close() + m.Lock() + if p.alSource != 0 { + al.DeleteSources(p.alSource) + p.alSource = 0 + } + if 0 < len(p.alBuffers) { + al.DeleteBuffers(p.alBuffers...) + p.alBuffers = []al.Buffer{} + } + if err := al.Error(); err != 0 { + panic(fmt.Sprintf("audio: close error: %d", err)) + } + m.Unlock() + runtime.SetFinalizer(p, nil) + return nil } // TODO: Implement Close method