mirror of
https://github.com/hajimehoshi/ebiten.git
synced 2024-12-24 10:48:53 +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.
|
// 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
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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
|
||||||
|
@ -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()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -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()
|
||||||
|
@ -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()
|
||||||
|
@ -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{}{}
|
||||||
}()
|
}()
|
||||||
|
Loading…
Reference in New Issue
Block a user