audio: Make the game loop depend on the audio clock

This commit is contained in:
Hajime Hoshi 2017-07-11 00:20:47 +09:00
parent 7c277c3ab3
commit fdaf03b209
2 changed files with 99 additions and 76 deletions

View File

@ -34,10 +34,11 @@ import (
"sync" "sync"
"time" "time"
"github.com/hajimehoshi/ebiten"
"github.com/hajimehoshi/oto" "github.com/hajimehoshi/oto"
) )
const FPS = 60
type players struct { type players struct {
players map[*Player]struct{} players map[*Player]struct{}
sync.RWMutex sync.RWMutex
@ -171,13 +172,13 @@ 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
playerWriteCh chan []uint8 errCh chan error
playerErrCh chan error pingCh chan struct{}
playerCloseCh chan struct{}
sampleRate int sampleRate int
frames int64 frames int64
framesReadOnly int64
writtenBytes int64 writtenBytes int64
discardingCount int m sync.Mutex
} }
var ( var (
@ -198,89 +199,86 @@ func NewContext(sampleRate int) (*Context, error) {
} }
c := &Context{ c := &Context{
sampleRate: sampleRate, sampleRate: sampleRate,
errCh: make(chan error, 1),
pingCh: make(chan struct{}),
} }
theContext = c theContext = c
c.players = &players{ c.players = &players{
players: map[*Player]struct{}{}, players: map[*Player]struct{}{},
} }
return c, nil
go c.loop()
return c, nil
} }
// Update proceeds the inner (logical) time of the context by 1/60 second. func CurrentContext() *Context {
// theContextLock.Lock()
// This is expected to be called in the game's updating function (sync mode) c := theContext
// or an independent goroutine with timers (async mode). theContextLock.Unlock()
// In sync mode, the game logical time syncs the audio logical time and return c
// you will find audio stops when the game stops e.g. when the window is deactivated. }
// In async mode, the audio never stops even when the game stops.
// // Internal Only?
// Update returns error when IO error occurs in the underlying IO object. func (c *Context) Frame() int64 {
func (c *Context) Update() error { c.m.Lock()
n := c.framesReadOnly
c.m.Unlock()
return n
}
// Internal Only?
func (c *Context) Ping() {
select {
case c.pingCh <- struct{}{}:
default:
}
}
func (c *Context) loop() {
// Initialize oto.Player lazily to enable calling NewContext in an 'init' function. // Initialize oto.Player lazily to enable calling NewContext in an 'init' function.
// Accessing oto.Player 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.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)
if err != nil { if err != nil {
init <- err c.errCh <- err
return
} }
defer p.Close() defer p.Close()
close(init)
for { for {
select { c.m.Lock()
case buf := <-c.playerWriteCh: c.framesReadOnly = c.frames
if _, err = p.Write(buf); err != nil { c.m.Unlock()
c.playerErrCh <- err if c.frames%10 == 0 {
} <-c.pingCh
case <-c.playerCloseCh:
return
}
}
}()
if err := <-init; err != nil {
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 / 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)
if _, err := io.ReadFull(c.players, buf); err != nil { if _, err := io.ReadFull(c.players, buf); err != nil {
close(c.playerCloseCh) c.errCh <- err
return err
} }
// Discard when the buffer queue seems full. if _, err = p.Write(buf); err != nil {
if c.discardingCount > 0 { c.errCh <- err
c.discardingCount--
return nil
} }
}
}
// Update returns an error if some errors happen.
func (c *Context) Update() error {
select { select {
case c.playerWriteCh <- buf: case err := <-c.errCh:
// Writing can block. Don't wait for the result here. return err
default: default:
// The current buffer size is 1/15 [sec] = 4 [frames].
// Wait for 5 [frames] which is more than 4.
c.discardingCount = 5
} }
return nil return nil
} }

View File

@ -18,6 +18,7 @@ import (
"errors" "errors"
"time" "time"
"github.com/hajimehoshi/ebiten/audio"
"github.com/hajimehoshi/ebiten/internal/sync" "github.com/hajimehoshi/ebiten/internal/sync"
"github.com/hajimehoshi/ebiten/internal/ui" "github.com/hajimehoshi/ebiten/internal/ui"
) )
@ -35,6 +36,10 @@ type runContext struct {
lastUpdated int64 lastUpdated int64
lastFPSUpdated int64 lastFPSUpdated int64
m sync.RWMutex m sync.RWMutex
lastAudioFrame int64
lastAudioFrameTime int64
deltaTime int64
} }
var currentRunContext *runContext var currentRunContext *runContext
@ -106,7 +111,7 @@ func Run(g GraphicsContext, width, height int, scale float64, title string, fps
currentRunContext.startRunning() currentRunContext.startRunning()
defer currentRunContext.endRunning() defer currentRunContext.endRunning()
n := now() n := currentRunContext.adjustedNowWithAudio()
currentRunContext.lastUpdated = n currentRunContext.lastUpdated = n
currentRunContext.lastFPSUpdated = n currentRunContext.lastFPSUpdated = n
@ -120,9 +125,26 @@ func Run(g GraphicsContext, width, height int, scale float64, title string, fps
return nil return nil
} }
func (c *runContext) adjustedNowWithAudio() int64 {
n := now()
if audio.CurrentContext() == nil {
return n
}
if c.lastAudioFrameTime == 0 {
c.lastAudioFrameTime = n
}
if f := audio.CurrentContext().Frame(); c.lastAudioFrame != f {
an := c.lastAudioFrameTime + (f-c.lastAudioFrame)*int64(time.Second)/audio.FPS
c.deltaTime += an - n
c.lastAudioFrame = f
c.lastAudioFrameTime = n
}
return n + c.deltaTime
}
func (c *runContext) render(g GraphicsContext) error { func (c *runContext) render(g GraphicsContext) error {
fps := c.fps fps := c.fps
n := now() n := c.adjustedNowWithAudio()
defer func() { defer func() {
// Calc the current FPS. // Calc the current FPS.
if time.Second > time.Duration(n-c.lastFPSUpdated) { if time.Second > time.Duration(n-c.lastFPSUpdated) {
@ -139,6 +161,9 @@ func (c *runContext) render(g GraphicsContext) error {
c.lastUpdated = n c.lastUpdated = n
return nil return nil
} }
if audio.CurrentContext() != nil {
audio.CurrentContext().Ping()
}
// Note that generally t is a little different from 1/60[sec]. // Note that generally t is a little different from 1/60[sec].
t := n - c.lastUpdated t := n - c.lastUpdated