mirror of
https://github.com/hajimehoshi/ebiten.git
synced 2024-12-24 18:58:54 +01:00
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:
parent
d88d1be4ad
commit
5976e4bbbc
@ -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
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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
|
||||
|
@ -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()
|
||||
}
|
||||
}
|
||||
|
@ -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()
|
||||
|
@ -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()
|
||||
|
@ -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{}{}
|
||||
}()
|
||||
|
Loading…
Reference in New Issue
Block a user