audio: Bug fix: Write can block and needs to be called asynchronously (#373)

This commit is contained in:
Hajime Hoshi 2017-06-29 03:36:19 +09:00
parent ba86d37ce8
commit 1fe2d90d8a

View File

@ -171,7 +171,9 @@ func (p *players) hasSource(src ReadSeekCloser) bool {
// In this case, audio goes on even when the game stops e.g. by diactivating the screen. // In this case, audio goes on even when the game stops e.g. by diactivating the screen.
type Context struct { type Context struct {
players *players players *players
driver *oto.Player playerWriteCh chan []uint8
playerErrCh chan error
playerCloseCh chan struct{}
sampleRate int sampleRate int
frames int64 frames int64
writtenBytes int64 writtenBytes int64
@ -214,40 +216,62 @@ func NewContext(sampleRate int) (*Context, error) {
// //
// Update returns error when IO error occurs in the underlying IO object. // Update returns error when IO error occurs in the underlying IO object.
func (c *Context) Update() error { func (c *Context) Update() error {
// Initialize c.driver lazily to enable calling NewContext in an 'init' function. // Initialize oto.Player lazily to enable calling NewContext in an 'init' function.
// Accessing driver functions requires the environment to be already initialized, // Accessing oto.Player functions requires the environment to be already initialized,
// but if Ebiten is used for a shared library, the timing when init functions are called // but if Ebiten is used for a shared library, the timing when init functions are called
// is unexpectable. // is unexpectable.
// e.g. a variable for JVM on Android might not be set. // e.g. a variable for JVM on Android might not be set.
if c.driver == nil { if c.playerWriteCh == nil {
init := make(chan error)
c.playerWriteCh = make(chan []uint8)
c.playerErrCh = make(chan error, 1)
c.playerCloseCh = make(chan struct{})
go func() {
// The buffer size is 1/15 sec. // The buffer size is 1/15 sec.
// It looks like 1/20 sec is too short for Android. // It looks like 1/20 sec is too short for Android.
s := c.sampleRate * channelNum * bytesPerSample / 15 s := c.sampleRate * channelNum * bytesPerSample / 15
p, err := oto.NewPlayer(c.sampleRate, channelNum, bytesPerSample, s) p, err := oto.NewPlayer(c.sampleRate, channelNum, bytesPerSample, s)
c.driver = p
if err != nil { if err != nil {
init <- err
return
}
defer p.Close()
close(init)
for {
select {
case buf := <-c.playerWriteCh:
if _, err = p.Write(buf); err != nil {
c.playerErrCh <- err
}
case <-c.playerCloseCh:
return
}
}
}()
if err := <-init; err != nil {
return err return err
} }
} }
select {
case err := <-c.playerErrCh:
close(c.playerCloseCh)
return err
default:
}
c.frames++ c.frames++
bytesPerFrame := c.sampleRate * bytesPerSample * channelNum / ebiten.FPS bytesPerFrame := c.sampleRate * bytesPerSample * channelNum / ebiten.FPS
l := (c.frames * int64(bytesPerFrame)) - c.writtenBytes l := (c.frames * int64(bytesPerFrame)) - c.writtenBytes
l &= mask l &= mask
c.writtenBytes += l c.writtenBytes += l
buf := make([]uint8, l) buf := make([]uint8, l)
n, err := io.ReadFull(c.players, buf) if _, err := io.ReadFull(c.players, buf); err != nil {
if err != nil { close(c.playerCloseCh)
return err return err
} }
if n != len(buf) { select {
return c.driver.Close() case c.playerWriteCh <- buf:
} // Writing can block. Don't wait for the result here.
_, err = c.driver.Write(buf) default:
if err == io.EOF {
return c.driver.Close()
}
if err != nil {
return err
} }
return nil return nil
} }