mirror of
https://github.com/hajimehoshi/ebiten.git
synced 2024-12-24 18:58:54 +01:00
audio: Make the game loop depend on the audio clock
This commit is contained in:
parent
7c277c3ab3
commit
fdaf03b209
146
audio/audio.go
146
audio/audio.go
@ -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
|
||||||
@ -170,14 +171,14 @@ func (p *players) hasSource(src ReadSeekCloser) bool {
|
|||||||
// You can also call Update independently from the game loop as 'async mode'.
|
// You can also call Update independently from the game loop as 'async mode'.
|
||||||
// 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,90 +199,87 @@ 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)
|
// The buffer size is 1/15 sec.
|
||||||
c.playerWriteCh = make(chan []uint8)
|
// It looks like 1/20 sec is too short for Android.
|
||||||
c.playerErrCh = make(chan error, 1)
|
s := c.sampleRate * channelNum * bytesPerSample / 15
|
||||||
c.playerCloseCh = make(chan struct{})
|
p, err := oto.NewPlayer(c.sampleRate, channelNum, bytesPerSample, s)
|
||||||
go func() {
|
if err != nil {
|
||||||
// The buffer size is 1/15 sec.
|
c.errCh <- err
|
||||||
// It looks like 1/20 sec is too short for Android.
|
}
|
||||||
s := c.sampleRate * channelNum * bytesPerSample / 15
|
defer p.Close()
|
||||||
p, err := oto.NewPlayer(c.sampleRate, channelNum, bytesPerSample, s)
|
|
||||||
if err != nil {
|
for {
|
||||||
init <- err
|
c.m.Lock()
|
||||||
return
|
c.framesReadOnly = c.frames
|
||||||
}
|
c.m.Unlock()
|
||||||
defer p.Close()
|
if c.frames%10 == 0 {
|
||||||
close(init)
|
<-c.pingCh
|
||||||
for {
|
}
|
||||||
select {
|
c.frames++
|
||||||
case buf := <-c.playerWriteCh:
|
bytesPerFrame := c.sampleRate * bytesPerSample * channelNum / FPS
|
||||||
if _, err = p.Write(buf); err != nil {
|
l := (c.frames * int64(bytesPerFrame)) - c.writtenBytes
|
||||||
c.playerErrCh <- err
|
l &= mask
|
||||||
}
|
c.writtenBytes += l
|
||||||
case <-c.playerCloseCh:
|
buf := make([]uint8, l)
|
||||||
return
|
if _, err := io.ReadFull(c.players, buf); err != nil {
|
||||||
}
|
c.errCh <- err
|
||||||
}
|
}
|
||||||
}()
|
if _, err = p.Write(buf); err != nil {
|
||||||
if err := <-init; err != nil {
|
c.errCh <- err
|
||||||
return err
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update returns an error if some errors happen.
|
||||||
|
func (c *Context) Update() error {
|
||||||
select {
|
select {
|
||||||
case err := <-c.playerErrCh:
|
case err := <-c.errCh:
|
||||||
close(c.playerCloseCh)
|
|
||||||
return err
|
return err
|
||||||
default:
|
default:
|
||||||
}
|
}
|
||||||
c.frames++
|
|
||||||
bytesPerFrame := c.sampleRate * bytesPerSample * channelNum / ebiten.FPS
|
|
||||||
l := (c.frames * int64(bytesPerFrame)) - c.writtenBytes
|
|
||||||
l &= mask
|
|
||||||
c.writtenBytes += l
|
|
||||||
buf := make([]uint8, l)
|
|
||||||
if _, err := io.ReadFull(c.players, buf); err != nil {
|
|
||||||
close(c.playerCloseCh)
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
// Discard when the buffer queue seems full.
|
|
||||||
if c.discardingCount > 0 {
|
|
||||||
c.discardingCount--
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
select {
|
|
||||||
case c.playerWriteCh <- buf:
|
|
||||||
// Writing can block. Don't wait for the result here.
|
|
||||||
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
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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
|
||||||
|
Loading…
Reference in New Issue
Block a user