audio: Add AudioContext.Update (#177)

This commit is contained in:
Hajime Hoshi 2016-03-11 00:01:00 +09:00
parent 77b62615b2
commit 71312ba26f
7 changed files with 36 additions and 8 deletions

View File

@ -35,6 +35,7 @@ var (
) )
func update(screen *ebiten.Image) error { func update(screen *ebiten.Image) error {
audioContext.Update()
if !audioLoaded { if !audioLoaded {
select { select {
case <-audioLoadingDone: case <-audioLoadingDone:

View File

@ -131,6 +131,7 @@ func addNote() error {
} }
func update(screen *ebiten.Image) error { func update(screen *ebiten.Image) error {
audioContext.Update()
defer func() { defer func() {
frames++ frames++
}() }()

View File

@ -201,6 +201,7 @@ func init() {
} }
func update(screen *ebiten.Image) error { func update(screen *ebiten.Image) error {
audioContext.Update()
updateInput() updateInput()
for i, key := range keys { for i, key := range keys {
if keyStates[key] != 1 { if keyStates[key] != 1 {

View File

@ -79,6 +79,7 @@ func (s *stream) Seek(offset int64, whence int) (int64, error) {
var player *audio.Player var player *audio.Player
func update(screen *ebiten.Image) error { func update(screen *ebiten.Image) error {
audioContext.Update()
if player == nil { if player == nil {
var err error var err error
player, err = audioContext.NewPlayer(&stream{}) player, err = audioContext.NewPlayer(&stream{})

View File

@ -23,7 +23,8 @@ import (
// TODO: In JavaScript, mixing should be done by WebAudio for performance. // TODO: In JavaScript, mixing should be done by WebAudio for performance.
type mixedPlayersStream struct { type mixedPlayersStream struct {
context *Context context *Context
writtenBytes int
} }
func min(a, b int) int { func min(a, b int) int {
@ -37,9 +38,19 @@ func (s *mixedPlayersStream) Read(b []byte) (int, error) {
s.context.Lock() s.context.Lock()
defer s.context.Unlock() defer s.context.Unlock()
// TODO: 60 (FPS) is a magic number
bytesPerFrame := s.context.sampleRate * 4 / 60
x := s.context.frames*bytesPerFrame + len(b)
if x <= s.writtenBytes {
return 0, nil
}
l := len(b) / 4 * 4 l := len(b) / 4 * 4
if len(s.context.players) == 0 { if len(s.context.players) == 0 {
return 0, nil l := min(len(b), x-s.writtenBytes)
copy(b, make([]byte, l))
s.writtenBytes += l
return l, nil
} }
closed := []*Player{} closed := []*Player{}
ll := l ll := l
@ -76,6 +87,7 @@ func (s *mixedPlayersStream) Read(b []byte) (int, error) {
for _, p := range closed { for _, p := range closed {
delete(s.context.players, p) delete(s.context.players, p)
} }
s.writtenBytes += ll
return ll, nil return ll, nil
} }
@ -86,6 +98,7 @@ type Context struct {
stream *mixedPlayersStream stream *mixedPlayersStream
players map[*Player]struct{} players map[*Player]struct{}
innerPlayer *player innerPlayer *player
frames int
sync.Mutex sync.Mutex
} }
@ -95,7 +108,9 @@ func NewContext(sampleRate int) *Context {
sampleRate: sampleRate, sampleRate: sampleRate,
players: map[*Player]struct{}{}, players: map[*Player]struct{}{},
} }
c.stream = &mixedPlayersStream{c} c.stream = &mixedPlayersStream{
context: c,
}
p, err := startPlaying(c.stream, c.sampleRate) p, err := startPlaying(c.stream, c.sampleRate)
if err != nil { if err != nil {
panic(fmt.Sprintf("audio: NewContext error: %v", err)) panic(fmt.Sprintf("audio: NewContext error: %v", err))
@ -104,6 +119,18 @@ func NewContext(sampleRate int) *Context {
return c return c
} }
// Update proceeds the inner (logical) time of the context by 1/60 second.
// This is expected to be called in the game's updating function (sync mode)
// or an independent goroutine with timers (unsync mode).
// In sync mode, the game logical time syncs the audio logical time and
// you will find audio stops when the game stops e.g. when the window is deactivated.
// In unsync mode, the audio never stops even when the game stops.
func (c *Context) Update() {
c.Lock()
defer c.Unlock()
c.frames++
}
type Player struct { type Player struct {
context *Context context *Context
src io.ReadSeeker src io.ReadSeeker

View File

@ -79,11 +79,6 @@ func (p *player) proceed() error {
const bytesPerSample = channelNum * 16 / 8 const bytesPerSample = channelNum * 16 / 8
bufferSize := p.sampleRate * bytesPerSample / 60 bufferSize := p.sampleRate * bytesPerSample / 60
c := int64(p.context.Get("currentTime").Float() * float64(p.sampleRate)) c := int64(p.context.Get("currentTime").Float() * float64(p.sampleRate))
// Buffer size is relatively big and it is needed to check that c.positionInSample doesn't
// proceed too far away (#180).
if c+int64(bufferSize) < p.positionInSamples {
return nil
}
if p.positionInSamples < c { if p.positionInSamples < c {
p.positionInSamples = c p.positionInSamples = c
} }

View File

@ -30,6 +30,8 @@ type VorbisStream struct {
buf *bytes.Reader buf *bytes.Reader
} }
// TODO: Rename to DecodeVorbis or Decode?
func (c *Context) NewVorbisStream(src io.Reader) (*VorbisStream, error) { func (c *Context) NewVorbisStream(src io.Reader) (*VorbisStream, error) {
decoded, channels, sampleRate, err := vorbis.Decode(src) decoded, channels, sampleRate, err := vorbis.Decode(src)
if err != nil { if err != nil {