audio: make (*Player).Position() smoother

Closes #2901
This commit is contained in:
Hajime Hoshi 2024-02-01 15:43:02 +09:00
parent 788529ff76
commit 6ced6987cd
3 changed files with 141 additions and 13 deletions

View File

@ -136,7 +136,7 @@ func NewContext(sampleRate int) *Context {
return err return err
} }
if err := c.gcPlayers(); err != nil { if err := c.updatePlayers(); err != nil {
return err return err
} }
return nil return nil
@ -197,7 +197,7 @@ func (c *Context) removePlayingPlayer(p *playerImpl) {
c.m.Unlock() c.m.Unlock()
} }
func (c *Context) gcPlayers() error { func (c *Context) updatePlayers() error {
// A Context must not call playerImpl's functions with a lock, or this causes a deadlock (#2737). // A Context must not call playerImpl's functions with a lock, or this causes a deadlock (#2737).
// Copy the playerImpls and iterate them without a lock. // Copy the playerImpls and iterate them without a lock.
var players []*playerImpl var players []*playerImpl
@ -218,6 +218,7 @@ func (c *Context) gcPlayers() error {
if err := p.Err(); err != nil { if err := p.Err(); err != nil {
return err return err
} }
p.updatePosition()
if !p.IsPlaying() { if !p.IsPlaying() {
playersToRemove = append(playersToRemove, p) playersToRemove = append(playersToRemove, p)
} }

View File

@ -69,6 +69,19 @@ type playerImpl struct {
stream *timeStream stream *timeStream
factory *playerFactory factory *playerFactory
initBufferSize int initBufferSize int
// adjustedPosition is the player's more accurate position.
// The underlying buffer might not be changed even if the player is playing.
// adjustedPosition is adjusted by the time duration during the player position doesn't change while its playing.
adjustedPosition time.Duration
// lastSamples is the last value of the number of samples.
// When lastSamples is a negative number, this value is not initialized yet.
lastSamples int64
// stopwatch is a stopwatch to measure the time duration during the player position doesn't change while its playing.
stopwatch stopwatch
m sync.Mutex m sync.Mutex
} }
@ -80,6 +93,7 @@ func (f *playerFactory) newPlayer(context *Context, src io.Reader) (*playerImpl,
src: src, src: src,
context: context, context: context,
factory: f, factory: f,
lastSamples: -1,
} }
runtime.SetFinalizer(p, (*playerImpl).Close) runtime.SetFinalizer(p, (*playerImpl).Close)
return p, nil return p, nil
@ -178,6 +192,7 @@ func (p *playerImpl) Play() {
} }
p.player.Play() p.player.Play()
p.context.addPlayingPlayer(p) p.context.addPlayingPlayer(p)
p.stopwatch.start()
} }
func (p *playerImpl) Pause() { func (p *playerImpl) Pause() {
@ -193,12 +208,16 @@ func (p *playerImpl) Pause() {
p.player.Pause() p.player.Pause()
p.context.removePlayingPlayer(p) p.context.removePlayingPlayer(p)
p.stopwatch.stop()
} }
func (p *playerImpl) IsPlaying() bool { func (p *playerImpl) IsPlaying() bool {
p.m.Lock() p.m.Lock()
defer p.m.Unlock() defer p.m.Unlock()
return p.isPlaying()
}
func (p *playerImpl) isPlaying() bool {
if p.player == nil { if p.player == nil {
return false return false
} }
@ -237,6 +256,7 @@ func (p *playerImpl) Close() error {
p.player = nil p.player = nil
}() }()
p.player.Pause() p.player.Pause()
p.stopwatch.stop()
return p.player.Close() return p.player.Close()
} }
return nil return nil
@ -245,13 +265,7 @@ func (p *playerImpl) Close() error {
func (p *playerImpl) Position() time.Duration { func (p *playerImpl) Position() time.Duration {
p.m.Lock() p.m.Lock()
defer p.m.Unlock() defer p.m.Unlock()
return p.adjustedPosition
if p.player == nil {
return 0
}
samples := (p.stream.position() - int64(p.player.BufferedSize())) / bytesPerSampleInt16
return time.Duration(samples) * time.Second / time.Duration(p.factory.sampleRate)
} }
func (p *playerImpl) Rewind() error { func (p *playerImpl) Rewind() error {
@ -263,6 +277,7 @@ func (p *playerImpl) SetPosition(offset time.Duration) error {
defer p.m.Unlock() defer p.m.Unlock()
if offset == 0 && p.player == nil { if offset == 0 && p.player == nil {
p.adjustedPosition = 0
return nil return nil
} }
@ -274,6 +289,13 @@ func (p *playerImpl) SetPosition(offset time.Duration) error {
if _, err := p.player.Seek(pos, io.SeekStart); err != nil { if _, err := p.player.Seek(pos, io.SeekStart); err != nil {
return err return err
} }
p.lastSamples = -1
// Just after setting a position, the buffer size should be 0 as no data is sent.
p.adjustedPosition = p.stream.positionInTimeDuration()
p.stopwatch.reset()
if p.isPlaying() {
p.stopwatch.start()
}
return nil return nil
} }
@ -304,6 +326,34 @@ func (p *playerImpl) source() io.Reader {
return p.src return p.src
} }
func (p *playerImpl) updatePosition() {
p.m.Lock()
defer p.m.Unlock()
if p.player == nil {
p.adjustedPosition = 0
return
}
samples := (p.stream.position() - int64(p.player.BufferedSize())) / bytesPerSampleInt16
var adjustingTime time.Duration
if p.lastSamples >= 0 && p.lastSamples == samples {
// If the number of samples is not changed from the last tick,
// the underlying buffer is not updated yet. Adjust the position by the time (#2901).
adjustingTime = p.stopwatch.current()
} else {
p.lastSamples = samples
p.stopwatch.reset()
if p.isPlaying() {
p.stopwatch.start()
}
}
// Update the adjusted position every tick. This is necessary to keep the position accurate.
p.adjustedPosition = time.Duration(samples)*time.Second/time.Duration(p.factory.sampleRate) + adjustingTime
}
type timeStream struct { type timeStream struct {
r io.Reader r io.Reader
sampleRate int sampleRate int
@ -376,3 +426,10 @@ func (s *timeStream) position() int64 {
return s.pos return s.pos
} }
func (s *timeStream) positionInTimeDuration() time.Duration {
s.m.Lock()
defer s.m.Unlock()
return time.Duration(s.pos) * time.Second / (time.Duration(s.sampleRate) * bytesPerSampleInt16)
}

70
audio/stopwatch.go Normal file
View File

@ -0,0 +1,70 @@
// Copyright 2024 The Ebitengine Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package audio
import (
"sync"
"time"
)
type stopwatch struct {
duration time.Duration
lastStarted time.Time
running bool
m sync.Mutex
}
func (s *stopwatch) start() {
s.m.Lock()
defer s.m.Unlock()
if s.running {
return
}
s.lastStarted = time.Now()
s.running = true
}
func (s *stopwatch) stop() {
s.m.Lock()
defer s.m.Unlock()
if !s.running {
return
}
s.duration += time.Since(s.lastStarted)
s.running = false
}
func (s *stopwatch) current() time.Duration {
s.m.Lock()
defer s.m.Unlock()
d := s.duration
if s.running {
d += time.Since(s.lastStarted)
}
return d
}
func (s *stopwatch) reset() {
s.m.Lock()
defer s.m.Unlock()
s.duration = 0
s.lastStarted = time.Time{}
s.running = false
}