audio: More intellegent suspending/resuming (#617)

Before this change, the audio is suspended when the game stops for
1/12[s]. However, as game often stops for more than 1/12[s]
especially on mobiles, this implemntation caused some audio
glitches.

This change fixes this problem by re-implementing suspending/
resumeing audio by detecting the window is active/focused or not.
This commit is contained in:
Hajime Hoshi 2018-05-26 18:04:20 +09:00
parent d88d1be4ad
commit 5976e4bbbc
6 changed files with 134 additions and 49 deletions

View File

@ -151,9 +151,6 @@ func (p *players) hasSource(src io.ReadCloser) bool {
// For a typical usage example, see examples/wav/main.go. // For a typical usage example, see examples/wav/main.go.
type Context struct { type Context struct {
players *players players *players
initCh chan struct{}
initedCh chan struct{}
pingCount int
sampleRate int sampleRate int
err error err error
@ -218,26 +215,20 @@ func CurrentContext() *Context {
return c return c
} }
func (c *Context) ping() {
if c.initCh != nil {
close(c.initCh)
c.initCh = nil
}
<-c.initedCh
c.m.Lock()
c.pingCount = 5
c.m.Unlock()
}
func (c *Context) loop() { func (c *Context) loop() {
c.initCh = make(chan struct{}) initCh := make(chan struct{})
c.initedCh = make(chan struct{})
// Copy the channel since c.initCh can be set as nil after clock.RegisterPing. suspendCh := make(chan struct{}, 1)
initCh := c.initCh resumeCh := make(chan struct{}, 1)
hooks.OnSuspendAudio(func() {
clock.RegisterPing(c.ping) suspendCh <- struct{}{}
})
hooks.OnResumeAudio(func() {
resumeCh <- struct{}{}
})
clock.OnStart(func() {
close(initCh)
})
// 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,
@ -255,31 +246,25 @@ func (c *Context) loop() {
} }
defer p.Close() defer p.Close()
close(c.initedCh)
bytesPerFrame := c.sampleRate * bytesPerSample * channelNum / clock.FPS bytesPerFrame := c.sampleRate * bytesPerSample * channelNum / clock.FPS
written := int64(0) written := int64(0)
prevWritten := int64(0) prevWritten := int64(0)
for { for {
c.m.Lock() select {
if c.pingCount == 0 { case <-suspendCh:
c.m.Unlock() <-resumeCh
time.Sleep(10 * time.Millisecond) default:
continue const n = 2048
} if _, err := io.CopyN(p, c.players, n); err != nil {
c.pingCount-- c.err = err
c.m.Unlock() return
}
const n = 2048 written += int64(n)
if _, err := io.CopyN(p, c.players, n); err != nil { fs := written/int64(bytesPerFrame) - prevWritten/int64(bytesPerFrame)
c.err = err clock.ProceedAudioTimer(fs)
return prevWritten = written
} }
written += int64(n)
fs := written/int64(bytesPerFrame) - prevWritten/int64(bytesPerFrame)
clock.ProceedAudioTimer(fs)
prevWritten = written
} }
} }

View File

@ -43,7 +43,8 @@ var (
lastFPSUpdated int64 lastFPSUpdated int64
framesForFPS int64 framesForFPS int64
ping func() started bool
onStart func()
m sync.Mutex m sync.Mutex
) )
@ -55,9 +56,9 @@ func CurrentFPS() float64 {
return v return v
} }
func RegisterPing(pingFunc func()) { func OnStart(f func()) {
m.Lock() m.Lock()
ping = pingFunc onStart = f
m.Unlock() m.Unlock()
} }
@ -87,12 +88,15 @@ func Update() int {
m.Lock() m.Lock()
defer m.Unlock() defer m.Unlock()
n := now() if !started {
if onStart != nil {
if ping != nil { onStart()
ping() }
started = true
} }
n := now()
// Initialize lastSystemTime if needed. // Initialize lastSystemTime if needed.
if lastSystemTime == 0 { if lastSystemTime == 0 {
lastSystemTime = n lastSystemTime = n

View File

@ -14,15 +14,26 @@
package hooks package hooks
import (
"sync"
)
var m sync.Mutex
var onBeforeUpdateHooks = []func() error{} var onBeforeUpdateHooks = []func() error{}
// AppendHookOnBeforeUpdate appends a hook function that is run before the main update function // AppendHookOnBeforeUpdate appends a hook function that is run before the main update function
// every frame. // every frame.
func AppendHookOnBeforeUpdate(f func() error) { func AppendHookOnBeforeUpdate(f func() error) {
m.Lock()
onBeforeUpdateHooks = append(onBeforeUpdateHooks, f) onBeforeUpdateHooks = append(onBeforeUpdateHooks, f)
m.Unlock()
} }
func RunBeforeUpdateHooks() error { func RunBeforeUpdateHooks() error {
m.Lock()
defer m.Unlock()
for _, f := range onBeforeUpdateHooks { for _, f := range onBeforeUpdateHooks {
if err := f(); err != nil { if err := f(); err != nil {
return err return err
@ -30,3 +41,45 @@ func RunBeforeUpdateHooks() error {
} }
return nil return nil
} }
var (
audioSuspended bool
onSuspendAudio func()
onResumeAudio func()
)
func OnSuspendAudio(f func()) {
m.Lock()
onSuspendAudio = f
m.Unlock()
}
func OnResumeAudio(f func()) {
m.Lock()
onResumeAudio = f
m.Unlock()
}
func SuspendAudio() {
m.Lock()
defer m.Unlock()
if audioSuspended {
return
}
audioSuspended = true
if onSuspendAudio != nil {
onSuspendAudio()
}
}
func ResumeAudio() {
m.Lock()
defer m.Unlock()
if !audioSuspended {
return
}
audioSuspended = false
if onResumeAudio != nil {
onResumeAudio()
}
}

View File

@ -29,6 +29,7 @@ import (
"github.com/go-gl/glfw/v3.2/glfw" "github.com/go-gl/glfw/v3.2/glfw"
"github.com/hajimehoshi/ebiten/internal/devicescale" "github.com/hajimehoshi/ebiten/internal/devicescale"
"github.com/hajimehoshi/ebiten/internal/hooks"
"github.com/hajimehoshi/ebiten/internal/input" "github.com/hajimehoshi/ebiten/internal/input"
"github.com/hajimehoshi/ebiten/internal/opengl" "github.com/hajimehoshi/ebiten/internal/opengl"
) )
@ -552,7 +553,9 @@ func (u *userInterface) update(g GraphicsContext) error {
_ = u.runOnMainThread(func() error { _ = u.runOnMainThread(func() error {
u.pollEvents() u.pollEvents()
defer hooks.ResumeAudio()
for !u.isRunnableInBackground() && u.window.GetAttrib(glfw.Focused) == 0 { for !u.isRunnableInBackground() && u.window.GetAttrib(glfw.Focused) == 0 {
hooks.SuspendAudio()
// Wait for an arbitrary period to avoid busy loop. // Wait for an arbitrary period to avoid busy loop.
time.Sleep(time.Second / 60) time.Sleep(time.Second / 60)
u.pollEvents() u.pollEvents()

View File

@ -23,6 +23,7 @@ import (
"github.com/gopherjs/gopherjs/js" "github.com/gopherjs/gopherjs/js"
"github.com/hajimehoshi/ebiten/internal/devicescale" "github.com/hajimehoshi/ebiten/internal/devicescale"
"github.com/hajimehoshi/ebiten/internal/hooks"
"github.com/hajimehoshi/ebiten/internal/input" "github.com/hajimehoshi/ebiten/internal/input"
"github.com/hajimehoshi/ebiten/internal/opengl" "github.com/hajimehoshi/ebiten/internal/opengl"
"github.com/hajimehoshi/ebiten/internal/web" "github.com/hajimehoshi/ebiten/internal/web"
@ -39,11 +40,13 @@ type userInterface struct {
sizeChanged bool sizeChanged bool
windowFocus bool windowFocus bool
pageVisible bool
} }
var currentUI = &userInterface{ var currentUI = &userInterface{
sizeChanged: true, sizeChanged: true,
windowFocus: true, windowFocus: true,
pageVisible: true,
} }
func MonitorSize() (int, int) { func MonitorSize() (int, int) {
@ -167,10 +170,17 @@ func (u *userInterface) updateGraphicsContext(g GraphicsContext) {
} }
} }
func (u *userInterface) suspended() bool {
return !u.runnableInBackground && (!u.windowFocus || !u.pageVisible)
}
func (u *userInterface) update(g GraphicsContext) error { func (u *userInterface) update(g GraphicsContext) error {
if !u.runnableInBackground && !u.windowFocus { if u.suspended() {
hooks.SuspendAudio()
return nil return nil
} }
hooks.ResumeAudio()
if opengl.GetContext().IsContextLost() { if opengl.GetContext().IsContextLost() {
opengl.GetContext().RestoreContext() opengl.GetContext().RestoreContext()
g.Invalidate() g.Invalidate()
@ -232,9 +242,27 @@ func initialize() error {
} }
window.Call("addEventListener", "focus", func() { window.Call("addEventListener", "focus", func() {
currentUI.windowFocus = true currentUI.windowFocus = true
if currentUI.suspended() {
hooks.SuspendAudio()
} else {
hooks.ResumeAudio()
}
}) })
window.Call("addEventListener", "blur", func() { window.Call("addEventListener", "blur", func() {
currentUI.windowFocus = false currentUI.windowFocus = false
if currentUI.suspended() {
hooks.SuspendAudio()
} else {
hooks.ResumeAudio()
}
})
doc.Call("addEventListener", "visibilitychange", func() {
currentUI.pageVisible = !doc.Get("hidden").Bool()
if currentUI.suspended() {
hooks.SuspendAudio()
} else {
hooks.ResumeAudio()
}
}) })
window.Call("addEventListener", "resize", func() { window.Call("addEventListener", "resize", func() {
currentUI.updateScreenSize() currentUI.updateScreenSize()

View File

@ -31,6 +31,7 @@ import (
"golang.org/x/mobile/gl" "golang.org/x/mobile/gl"
"github.com/hajimehoshi/ebiten/internal/devicescale" "github.com/hajimehoshi/ebiten/internal/devicescale"
"github.com/hajimehoshi/ebiten/internal/hooks"
"github.com/hajimehoshi/ebiten/internal/input" "github.com/hajimehoshi/ebiten/internal/input"
"github.com/hajimehoshi/ebiten/internal/opengl" "github.com/hajimehoshi/ebiten/internal/opengl"
) )
@ -202,7 +203,18 @@ func (u *userInterface) scaleImpl() float64 {
} }
func (u *userInterface) update(g GraphicsContext) error { func (u *userInterface) update(g GraphicsContext) error {
<-renderCh render:
for {
select {
case <-renderCh:
break render
case <-time.After(500 * time.Millisecond):
hooks.SuspendAudio()
continue
}
}
hooks.ResumeAudio()
defer func() { defer func() {
renderChEnd <- struct{}{} renderChEnd <- struct{}{}
}() }()