381 lines
9.5 KiB
Go
381 lines
9.5 KiB
Go
// Copyright 2018 The Ebiten 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 (
|
|
"io"
|
|
"sync"
|
|
"time"
|
|
)
|
|
|
|
type (
|
|
dummyContext struct {
|
|
// suspendErr and resumeErr are the errors Suspend and Resume return, to simulate a device
|
|
// which fails to suspend or resume.
|
|
suspendErr error
|
|
resumeErr error
|
|
|
|
// players are the players created since the context was reset last. ResetContextForTesting
|
|
// stops them, so that a player reading an infinite source does not keep reading after its test.
|
|
players []*dummyPlayer
|
|
|
|
mu sync.Mutex
|
|
}
|
|
dummyPlayer struct {
|
|
r io.Reader
|
|
playing bool
|
|
volume float64
|
|
|
|
// eof is whether the source has been exhausted by playing through. Like the real players,
|
|
// a player which has finished its source refuses to play until Seek resets this state.
|
|
eof bool
|
|
|
|
// drained is whether the simulated device has output the data it had buffered when the
|
|
// source was exhausted. See BufferedSize.
|
|
drained bool
|
|
|
|
// readGen is incremented by PauseAndStopReading to stop the goroutine reading r.
|
|
readGen int
|
|
|
|
// readers tracks the goroutines reading r, so that a test can wait for them to stop.
|
|
readers sync.WaitGroup
|
|
|
|
// err is the first non-EOF error the source returned. Like the real players, a player
|
|
// whose source failed stops for good and reports the error from Err.
|
|
err error
|
|
|
|
mu sync.Mutex
|
|
}
|
|
)
|
|
|
|
func (c *dummyContext) NewPlayer(r io.Reader) player {
|
|
p := &dummyPlayer{
|
|
r: r,
|
|
volume: 1,
|
|
}
|
|
c.mu.Lock()
|
|
defer c.mu.Unlock()
|
|
c.players = append(c.players, p)
|
|
return p
|
|
}
|
|
|
|
// stopPlayers stops the players' goroutines reading their sources, and waits for them to exit.
|
|
func (c *dummyContext) stopPlayers() {
|
|
for _, p := range c.takePlayers() {
|
|
p.PauseAndStopReading()
|
|
p.readers.Wait()
|
|
}
|
|
}
|
|
|
|
func (c *dummyContext) takePlayers() []*dummyPlayer {
|
|
c.mu.Lock()
|
|
defer c.mu.Unlock()
|
|
players := c.players
|
|
c.players = nil
|
|
return players
|
|
}
|
|
|
|
func (c *dummyContext) MaxBufferSize() int {
|
|
return 48000 * channelCount * bitDepthInBytesInt16 / 4
|
|
}
|
|
|
|
func (c *dummyContext) Suspend() error {
|
|
c.mu.Lock()
|
|
defer c.mu.Unlock()
|
|
return c.suspendErr
|
|
}
|
|
|
|
func (c *dummyContext) Resume() error {
|
|
c.mu.Lock()
|
|
defer c.mu.Unlock()
|
|
return c.resumeErr
|
|
}
|
|
|
|
func (c *dummyContext) Err() error {
|
|
return nil
|
|
}
|
|
|
|
func (p *dummyPlayer) Pause() {
|
|
p.mu.Lock()
|
|
p.playing = false
|
|
p.mu.Unlock()
|
|
}
|
|
|
|
func (p *dummyPlayer) Play() {
|
|
p.mu.Lock()
|
|
defer p.mu.Unlock()
|
|
|
|
if p.eof || p.err != nil {
|
|
return
|
|
}
|
|
p.playing = true
|
|
gen := p.readGen
|
|
p.readers.Go(func() {
|
|
var buf [4096]byte
|
|
for {
|
|
stopped, err := p.readOnce(gen, buf[:])
|
|
if stopped {
|
|
return
|
|
}
|
|
if err != nil {
|
|
break
|
|
}
|
|
time.Sleep(time.Millisecond)
|
|
}
|
|
p.mu.Lock()
|
|
defer p.mu.Unlock()
|
|
// The source is exhausted only when it was played through. A paused player would still
|
|
// have unplayed data in its buffer in a real player.
|
|
if p.playing {
|
|
p.eof = true
|
|
}
|
|
})
|
|
}
|
|
|
|
// readOnce performs one read from the source with the mutex held, so that PauseAndStopReading waits
|
|
// for an in-flight read. stopped reports that reading must not continue: PauseAndStopReading was
|
|
// called, or the source failed. err is io.EOF when the source is exhausted.
|
|
func (p *dummyPlayer) readOnce(gen int, buf []byte) (stopped bool, err error) {
|
|
p.mu.Lock()
|
|
defer p.mu.Unlock()
|
|
|
|
if p.readGen != gen {
|
|
return true, nil
|
|
}
|
|
_, err = p.r.Read(buf)
|
|
if err != nil && err != io.EOF {
|
|
p.err = err
|
|
p.playing = false
|
|
return true, nil
|
|
}
|
|
return false, err
|
|
}
|
|
|
|
func (p *dummyPlayer) PauseAndStopReading() {
|
|
p.mu.Lock()
|
|
p.playing = false
|
|
p.readGen++
|
|
p.mu.Unlock()
|
|
}
|
|
|
|
func (p *dummyPlayer) IsPlaying() bool {
|
|
p.mu.Lock()
|
|
defer p.mu.Unlock()
|
|
return p.playing
|
|
}
|
|
|
|
func (p *dummyPlayer) Volume() float64 {
|
|
// The volume is not shared with the goroutine Play spawns, but the mutex is held so that the
|
|
// test double is uniformly safe like the real player it stands in for.
|
|
p.mu.Lock()
|
|
defer p.mu.Unlock()
|
|
return p.volume
|
|
}
|
|
|
|
func (p *dummyPlayer) SetVolume(volume float64) {
|
|
// See Volume for why the mutex is held.
|
|
p.mu.Lock()
|
|
defer p.mu.Unlock()
|
|
p.volume = volume
|
|
}
|
|
|
|
// BufferedSize always reports an empty buffer, as the simulated device consumes the data as soon as
|
|
// it is read.
|
|
//
|
|
// The context calls this exactly once per tick, before it reads the position and checks IsPlaying.
|
|
// The player keeps playing for one more tick after its source is exhausted, so that the position
|
|
// reaches its final value while the player is still playing, as a real player which still has
|
|
// buffered data to output.
|
|
func (p *dummyPlayer) BufferedSize() int {
|
|
p.mu.Lock()
|
|
defer p.mu.Unlock()
|
|
|
|
if p.eof {
|
|
if p.drained {
|
|
p.playing = false
|
|
}
|
|
p.drained = true
|
|
}
|
|
return 0
|
|
}
|
|
|
|
func (p *dummyPlayer) Err() error {
|
|
p.mu.Lock()
|
|
defer p.mu.Unlock()
|
|
return p.err
|
|
}
|
|
|
|
func (p *dummyPlayer) SetBufferSize(bufferSize int) {
|
|
}
|
|
|
|
func (p *dummyPlayer) Seek(offset int64, whence int) (int64, error) {
|
|
// Seeking discards the buffered data and resets the finished state as real players do, so
|
|
// that the source can be played again.
|
|
p.mu.Lock()
|
|
defer p.mu.Unlock()
|
|
p.eof = false
|
|
p.drained = false
|
|
return 0, nil
|
|
}
|
|
|
|
var dummyContextForTesting = &dummyContext{}
|
|
|
|
func init() {
|
|
driverForTesting = dummyContextForTesting
|
|
}
|
|
|
|
// SetSuspendErrorForTesting makes the simulated device fail to suspend with err, or succeed when err
|
|
// is nil.
|
|
func SetSuspendErrorForTesting(err error) {
|
|
c := dummyContextForTesting
|
|
c.mu.Lock()
|
|
defer c.mu.Unlock()
|
|
c.suspendErr = err
|
|
}
|
|
|
|
// SetResumeErrorForTesting makes the simulated device fail to resume with err, or succeed when err is
|
|
// nil.
|
|
func SetResumeErrorForTesting(err error) {
|
|
c := dummyContextForTesting
|
|
c.mu.Lock()
|
|
defer c.mu.Unlock()
|
|
c.resumeErr = err
|
|
}
|
|
|
|
type dummyHook struct {
|
|
onSuspend func() error
|
|
onResume func() error
|
|
updates []func(vmGuest bool) error
|
|
}
|
|
|
|
func (h *dummyHook) OnSuspendAudio(f func() error) {
|
|
h.onSuspend = f
|
|
}
|
|
|
|
func (h *dummyHook) OnResumeAudio(f func() error) {
|
|
h.onResume = f
|
|
}
|
|
|
|
func (h *dummyHook) AppendHookOnBeforeUpdateWithVMGuestInfo(f func(vmGuest bool) error) {
|
|
h.updates = append(h.updates, f)
|
|
}
|
|
|
|
func init() {
|
|
hookerForTesting = &dummyHook{}
|
|
}
|
|
|
|
// SuspendForTesting runs the suspend hook the current context registered.
|
|
func SuspendForTesting() error {
|
|
return hookerForTesting.(*dummyHook).onSuspend()
|
|
}
|
|
|
|
// ResumeForTesting runs the resume hook the current context registered.
|
|
func ResumeForTesting() error {
|
|
return hookerForTesting.(*dummyHook).onResume()
|
|
}
|
|
|
|
func UpdateForTesting() error {
|
|
for _, f := range hookerForTesting.(*dummyHook).updates {
|
|
if err := f(false); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func PlayersCountForTesting() int {
|
|
c := CurrentContext()
|
|
c.m.Lock()
|
|
n := len(c.playingPlayers)
|
|
c.m.Unlock()
|
|
return n
|
|
}
|
|
|
|
func EnsurePlayerForTesting(p *Player) error {
|
|
// ensurePlayer accesses player state with its caller holding the mutex, as Play and startIfPending do.
|
|
p.p.m.Lock()
|
|
defer p.p.m.Unlock()
|
|
return p.p.ensurePlayer()
|
|
}
|
|
|
|
func BufferSizeForTesting(p *Player) int {
|
|
// initBufferSize is protected by the player mutex and can change when the underlying player is created.
|
|
p.p.m.Lock()
|
|
defer p.p.m.Unlock()
|
|
return p.p.initBufferSize
|
|
}
|
|
|
|
// PlayingButUntrackedForTesting reports whether the player is playing but is not tracked by its
|
|
// context as a playing player. This must never be true: an untracked playing player is not
|
|
// updated by the context and is not guarded from being garbage-collected.
|
|
//
|
|
// The player's lock is held across both checks, so that a player which is being started or
|
|
// stopped concurrently is not reported.
|
|
func PlayingButUntrackedForTesting(p *Player) bool {
|
|
pi := p.p
|
|
|
|
// isPlaying requires its caller to hold the player mutex. Keep it held across both checks.
|
|
pi.m.Lock()
|
|
defer pi.m.Unlock()
|
|
|
|
if pi.closed || !pi.isPlaying() {
|
|
return false
|
|
}
|
|
|
|
c := pi.context
|
|
// The context mutex protects playingPlayers from concurrent additions and removals.
|
|
c.m.Lock()
|
|
defer c.m.Unlock()
|
|
_, ok := c.playingPlayers[pi]
|
|
return !ok
|
|
}
|
|
|
|
// ContextCreatedForTesting reports whether the underlying audio device has been created.
|
|
func ContextCreatedForTesting() bool {
|
|
c := CurrentContext()
|
|
if c == nil {
|
|
return false
|
|
}
|
|
return c.playerFactory.currentContext() != nil
|
|
}
|
|
|
|
// ResetContextForTesting discards the current context, so that a test can create a new one.
|
|
// ResetContextForTesting stops the goroutine updating the players and the goroutines reading the
|
|
// players' sources, and waits for them to exit.
|
|
func ResetContextForTesting() {
|
|
if c := takeContextForTesting(); c != nil {
|
|
c.close()
|
|
}
|
|
dummyContextForTesting.stopPlayers()
|
|
}
|
|
|
|
func takeContextForTesting() *Context {
|
|
theContextLock.Lock()
|
|
defer theContextLock.Unlock()
|
|
c := theContext
|
|
theContext = nil
|
|
return c
|
|
}
|
|
|
|
func (i *InfiniteLoop) SetNoBlendForTesting(value bool) {
|
|
i.noBlendForTesting = value
|
|
}
|
|
|
|
// NewTimeStreamForTesting returns the stream a Player reads from, over a seekable src in signed 16bit
|
|
// integer stereo.
|
|
func NewTimeStreamForTesting(src io.ReadSeeker, sampleRate int) (io.ReadSeeker, error) {
|
|
return newTimeStream(src, true, sampleRate, bitDepthInBytesInt16)
|
|
}
|