// 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) }