Files
hajimehoshi-ebiten/audio/export_test.go
T

342 lines
8.3 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
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
// 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 {
return &dummyPlayer{
r: r,
volume: 1,
}
}
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
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
}
func ResetContextForTesting() {
theContextLock.Lock()
defer theContextLock.Unlock()
theContext = nil
}
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)
}