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.
type Context struct {
players *players
initCh chan struct{}
initedCh chan struct{}
pingCount int
sampleRate int
err error
@ -218,26 +215,20 @@ func CurrentContext() *Context {
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() {
c.initCh = make(chan struct{})
c.initedCh = make(chan struct{})
initCh := make(chan struct{})
// Copy the channel since c.initCh can be set as nil after clock.RegisterPing.
initCh := c.initCh
clock.RegisterPing(c.ping)
suspendCh := make(chan struct{}, 1)
resumeCh := make(chan struct{}, 1)
hooks.OnSuspendAudio(func() {
suspendCh <- struct{}{}
})
hooks.OnResumeAudio(func() {
resumeCh <- struct{}{}
})
clock.OnStart(func() {
close(initCh)
})
// Initialize oto.Player lazily to enable calling NewContext in an 'init' function.
// Accessing oto.Player functions requires the environment to be already initialized,
@ -255,31 +246,25 @@ func (c *Context) loop() {
}
defer p.Close()
close(c.initedCh)
bytesPerFrame := c.sampleRate * bytesPerSample * channelNum / clock.FPS
written := int64(0)
prevWritten := int64(0)
for {
c.m.Lock()
if c.pingCount == 0 {
c.m.Unlock()
time.Sleep(10 * time.Millisecond)
continue
}
c.pingCount--
c.m.Unlock()
select {
case <-suspendCh:
<-resumeCh
default:
const n = 2048
if _, err := io.CopyN(p, c.players, n); err != nil {
c.err = err
return
}
const n = 2048
if _, err := io.CopyN(p, c.players, n); err != nil {
c.err = err
return
written += int64(n)
fs := written/int64(bytesPerFrame) - prevWritten/int64(bytesPerFrame)
clock.ProceedAudioTimer(fs)
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
framesForFPS int64
ping func()
started bool
onStart func()
m sync.Mutex
)
@ -55,9 +56,9 @@ func CurrentFPS() float64 {
return v
}
func RegisterPing(pingFunc func()) {
func OnStart(f func()) {
m.Lock()
ping = pingFunc
onStart = f
m.Unlock()
}
@ -87,12 +88,15 @@ func Update() int {
m.Lock()
defer m.Unlock()
n := now()
if ping != nil {
ping()
if !started {
if onStart != nil {
onStart()
}
started = true
}
n := now()
// Initialize lastSystemTime if needed.
if lastSystemTime == 0 {
lastSystemTime = n

View File

@ -14,15 +14,26 @@
package hooks
import (
"sync"
)
var m sync.Mutex
var onBeforeUpdateHooks = []func() error{}
// AppendHookOnBeforeUpdate appends a hook function that is run before the main update function
// every frame.
func AppendHookOnBeforeUpdate(f func() error) {
m.Lock()
onBeforeUpdateHooks = append(onBeforeUpdateHooks, f)
m.Unlock()
}
func RunBeforeUpdateHooks() error {
m.Lock()
defer m.Unlock()
for _, f := range onBeforeUpdateHooks {
if err := f(); err != nil {
return err
@ -30,3 +41,45 @@ func RunBeforeUpdateHooks() error {
}
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/hajimehoshi/ebiten/internal/devicescale"
"github.com/hajimehoshi/ebiten/internal/hooks"
"github.com/hajimehoshi/ebiten/internal/input"
"github.com/hajimehoshi/ebiten/internal/opengl"
)
@ -552,7 +553,9 @@ func (u *userInterface) update(g GraphicsContext) error {
_ = u.runOnMainThread(func() error {
u.pollEvents()
defer hooks.ResumeAudio()
for !u.isRunnableInBackground() && u.window.GetAttrib(glfw.Focused) == 0 {
hooks.SuspendAudio()
// Wait for an arbitrary period to avoid busy loop.
time.Sleep(time.Second / 60)
u.pollEvents()

View File

@ -23,6 +23,7 @@ import (
"github.com/gopherjs/gopherjs/js"
"github.com/hajimehoshi/ebiten/internal/devicescale"
"github.com/hajimehoshi/ebiten/internal/hooks"
"github.com/hajimehoshi/ebiten/internal/input"
"github.com/hajimehoshi/ebiten/internal/opengl"
"github.com/hajimehoshi/ebiten/internal/web"
@ -39,11 +40,13 @@ type userInterface struct {
sizeChanged bool
windowFocus bool
pageVisible bool
}
var currentUI = &userInterface{
sizeChanged: true,
windowFocus: true,
pageVisible: true,
}
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 {
if !u.runnableInBackground && !u.windowFocus {
if u.suspended() {
hooks.SuspendAudio()
return nil
}
hooks.ResumeAudio()
if opengl.GetContext().IsContextLost() {
opengl.GetContext().RestoreContext()
g.Invalidate()
@ -232,9 +242,27 @@ func initialize() error {
}
window.Call("addEventListener", "focus", func() {
currentUI.windowFocus = true
if currentUI.suspended() {
hooks.SuspendAudio()
} else {
hooks.ResumeAudio()
}
})
window.Call("addEventListener", "blur", func() {
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() {
currentUI.updateScreenSize()

View File

@ -31,6 +31,7 @@ import (
"golang.org/x/mobile/gl"
"github.com/hajimehoshi/ebiten/internal/devicescale"
"github.com/hajimehoshi/ebiten/internal/hooks"
"github.com/hajimehoshi/ebiten/internal/input"
"github.com/hajimehoshi/ebiten/internal/opengl"
)
@ -202,7 +203,18 @@ func (u *userInterface) scaleImpl() float64 {
}
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() {
renderChEnd <- struct{}{}
}()