From 87bac3749b7c2fd4a49bf4752b4865a14fedb910 Mon Sep 17 00:00:00 2001 From: upstream Date: Fri, 2 Oct 2026 16:34:40 +0200 Subject: [PATCH] Upstream hajimehoshi/ebiten state of the touched files before 040fcc781c --- audio/audio.go | 743 ++++++++++++++++++++++++++++++++ audio/audio_test.go | 997 +++++++++++++++++++++++++++++++++++++++++++ audio/export_test.go | 341 +++++++++++++++ 3 files changed, 2081 insertions(+) create mode 100644 audio/audio.go create mode 100644 audio/audio_test.go create mode 100644 audio/export_test.go diff --git a/audio/audio.go b/audio/audio.go new file mode 100644 index 0000000..8f5e65e --- /dev/null +++ b/audio/audio.go @@ -0,0 +1,743 @@ +// Copyright 2015 Hajime Hoshi +// +// 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 provides audio players. +// +// This package must be used within an Ebitengine game; it does not work standalone, as the +// audio device is initialized only after the game starts running. To play audio outside an +// Ebitengine game, use Oto ([github.com/ebitengine/oto/v3]) directly. +// +// The stream format must be 16-bit little endian or 32-bit float little endian, and 2 channels. The format is as follows: +// +// [data] = [sample 1] [sample 2] [sample 3] ... +// [sample *] = [channel 1] [channel 2] +// [channel *] = [byte 1] [byte 2] ... +// +// A channel is 2 bytes in the 16-bit integer format, and 4 bytes in the 32-bit float format. +// +// An audio context (audio.Context object) has a sample rate you can specify +// and all streams you want to play must have the same sample rate. +// +// An audio context can generate 'players' (audio.Player objects), +// and you can play sound by calling Play function of players. +// When multiple players play, mixing is automatically done. +// Note that too many players may cause distortion. +// +// For the simplest example to play sound, see the wav package in the examples. +package audio + +import ( + "bytes" + "errors" + "fmt" + "io" + "reflect" + "runtime" + "sync" + "time" + + "github.com/hajimehoshi/ebiten/v2/audio/internal/convert" + "github.com/hajimehoshi/ebiten/v2/internal/hook" +) + +const ( + channelCount = 2 + bitDepthInBytesInt16 = 2 + bitDepthInBytesFloat32 = 4 +) + +// A Context represents a current state of audio. +// +// At most one Context object can exist in one process. +// This means only one constant sample rate is valid in your one application. +// +// For a typical usage example, see examples/wav/main.go. +type Context struct { + playerFactory *playerFactory + + sampleRate int + err error + ready bool + + playingPlayers map[*playerImpl]struct{} + + m sync.Mutex + // cond is signalled under m when a player is added to playingPlayers. The goroutine + // updating the players waits on it while playingPlayers is empty. + cond *sync.Cond +} + +var ( + theContext *Context + theContextLock sync.Mutex +) + +// NewContext creates a new audio context with the given sample rate. +// +// sampleRate specifies the number of samples that should be played during one second. +// Usual numbers are 44100 or 48000. One context has only one sample rate. You cannot play multiple audio +// sources with different sample rates at the same time. +// +// NewContext panics when sampleRate is not positive or an audio context is already created. +func NewContext(sampleRate int) *Context { + if sampleRate <= 0 { + panic(fmt.Sprintf("audio: sample rate must be positive but was %d", sampleRate)) + } + + theContextLock.Lock() + defer theContextLock.Unlock() + + if theContext != nil { + panic("audio: context is already created") + } + + c := &Context{ + sampleRate: sampleRate, + playerFactory: newPlayerFactory(sampleRate), + playingPlayers: map[*playerImpl]struct{}{}, + } + c.cond = sync.NewCond(&c.m) + theContext = c + + h := getHook() + h.OnSuspendAudio(func() error { + if err := c.playerFactory.suspend(); err != nil { + return err + } + if err := c.onSuspend(); err != nil { + return err + } + return nil + }) + h.OnResumeAudio(func() error { + if err := c.playerFactory.resume(); err != nil { + return err + } + if err := c.onResume(); err != nil { + return err + } + return nil + }) + + h.AppendHookOnBeforeUpdateWithVMGuestInfo(func(vmGuest bool) error { + var err error + theContextLock.Lock() + if theContext != nil { + err = theContext.error() + } + theContextLock.Unlock() + if err != nil { + return err + } + + // Create the audio device here, on the first update after the UI backend is + // initialized, rather than when a player is first touched. Deferring the creation + // keeps the device from being created before the environment is known (#969, #970, + // #2715, #3438). This also initializes the device when there is no player and the + // program waits for IsReady() to be true. + ready, err := c.playerFactory.initContextIfNeeded(vmGuest) + if err != nil { + return err + } + if ready != nil { + go func() { + <-ready + c.setReady() + }() + } + return nil + }) + + // In the current Ebitengine implementation, update might not be called when the window is in the background (#3154). + // In this case, an audio player's position is not updated correctly with AppendHookOnBeforeUpdateWithVMGuestInfo. + // Use a distinct goroutine to update the player states. + go func() { + for { + // Block while there is no player to update, so that an idle context does not wake + // up periodically. The sweep itself can remove the last player, so the check is + // repeated after every sweep. + c.waitForPlayingPlayers() + // A failure is local to the player which reported it. Record it and keep updating + // the other players (#3647). + if err := c.updatePlayers(); err != nil { + c.setError(err) + } + time.Sleep(time.Second / 100) + } + }() + + return c +} + +// CurrentContext returns the current context or nil if there is no context. +func CurrentContext() *Context { + theContextLock.Lock() + c := theContext + theContextLock.Unlock() + return c +} + +func (c *Context) setError(err error) { + c.m.Lock() + defer c.m.Unlock() + c.err = errors.Join(c.err, err) +} + +func (c *Context) error() error { + c.m.Lock() + defer c.m.Unlock() + if c.err != nil { + return fmt.Errorf("audio: context error: %w", c.err) + } + if err := c.playerFactory.error(); err != nil { + return fmt.Errorf("audio: context error: %w", err) + } + return nil +} + +func (c *Context) setReady() { + c.m.Lock() + c.ready = true + c.m.Unlock() +} + +// waitForPlayingPlayers blocks until at least one player is registered as playing. +func (c *Context) waitForPlayingPlayers() { + c.m.Lock() + defer c.m.Unlock() + for len(c.playingPlayers) == 0 { + c.cond.Wait() + } +} + +func (c *Context) addPlayingPlayer(p *playerImpl) { + c.m.Lock() + defer c.m.Unlock() + c.playingPlayers[p] = struct{}{} + c.cond.Signal() + + // An uncomparable ident is out of the duplication check (#3039). + ident := p.sourceIdent() + if !reflect.ValueOf(ident).Comparable() { + return + } + + // Check the source duplication. The players other than p have been checked against each + // other when they were added, so it is enough to compare p with them. + for playing := range c.playingPlayers { + if playing == p { + continue + } + playingIdent := playing.sourceIdent() + if !reflect.ValueOf(playingIdent).Comparable() { + continue + } + if playingIdent == ident { + c.err = errors.Join(c.err, errors.New("audio: the same source must not be used by multiple Player objects")) + return + } + } +} + +func (c *Context) removePlayingPlayer(p *playerImpl) { + c.m.Lock() + delete(c.playingPlayers, p) + c.m.Unlock() +} + +func (c *Context) onSuspend() error { + // 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. + var players []*playerImpl + c.m.Lock() + players = make([]*playerImpl, 0, len(c.playingPlayers)) + for p := range c.playingPlayers { + players = append(players, p) + } + c.m.Unlock() + + for _, p := range players { + if err := p.Err(); err != nil { + return err + } + p.onContextSuspended() + } + + return nil +} + +func (c *Context) onResume() error { + // 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. + var players []*playerImpl + c.m.Lock() + players = make([]*playerImpl, 0, len(c.playingPlayers)) + for p := range c.playingPlayers { + players = append(players, p) + } + c.m.Unlock() + + for _, p := range players { + if err := p.Err(); err != nil { + return err + } + p.onContextResumed() + } + + return nil +} + +func (c *Context) updatePlayers() error { + // 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. + var players []*playerImpl + c.m.Lock() + players = make([]*playerImpl, 0, len(c.playingPlayers)) + for p := range c.playingPlayers { + players = append(players, p) + } + c.m.Unlock() + + // In the current implementation, players cannot remove themselves from the context when the underlying player stops by itself. + // The underlying player can become paused after finishing playback, + // but there is no way to notify this to players so far. + // Instead, let's check the states proactively every frame. + // + // A failing player must not stop the sweep: the other players still need their positions + // updated and their pending plays started (#3647). Collect the errors and go on. + var errs []error + for _, p := range players { + if err := p.Err(); err != nil { + errs = append(errs, err) + // A player whose device player failed does not play again. Drop it from the playing + // players so that its error is reported once, not on every sweep. + c.removePlayingPlayer(p) + continue + } + // Start playing if Play was requested before the audio device was created. + if err := p.startIfPending(); err != nil { + errs = append(errs, err) + continue + } + p.updatePosition() + // The player itself decides whether it is removed, so that a player restarted between + // the check and the removal is not dropped from the context. + p.removeFromContextIfNotPlaying() + } + + return errors.Join(errs...) +} + +// IsReady returns a boolean value indicating whether the audio is ready or not. +// +// On some browsers, user interaction like click or pressing keys is required to start audio. +func (c *Context) IsReady() bool { + c.m.Lock() + defer c.m.Unlock() + return c.ready +} + +// SampleRate returns the sample rate. +func (c *Context) SampleRate() int { + return c.sampleRate +} + +// Player is an audio player which has one stream. +// +// Even when all references to a Player object are gone, +// the object is not GCed until the player finishes playing. +// This means that if a Player plays an infinite stream, +// the object is never GCed unless [Player.Pause] or [Player.PauseAndStopReading] is called. +type Player struct { + p *playerImpl + cleanup runtime.Cleanup +} + +// NewPlayer creates a new player with the given stream. +// +// src's format must be linear PCM (signed 16-bit little endian, 2 channel stereo) +// without a header (e.g. RIFF header). +// The sample rate must be same as that of the audio context. +// +// The player is seekable when src is io.Seeker. +// Attempt to seek the player that is not io.Seeker causes panic. +// +// Note that the given src can't be shared with other Player objects. +// +// NewPlayer does not touch src. The player asks src for its current position when it +// first uses src, at [Player.Play] or [Player.SetPosition]. +// +// A Player doesn't close src even if src implements io.Closer. +// Closing the source is src owner's responsibility. +// +// For new code, NewPlayerF32 is preferable to NewPlayer, since Ebitengine will treat only 32bit float audio internally in the future. +// +// A Player for 16bit integer must be used with 16bit integer version of audio APIs, like vorbis.DecodeWithoutResampling or audio.NewInfiniteLoop, not vorbis.DecodeF32 or audio.NewInfiniteLoopF32. +func (c *Context) NewPlayer(src io.Reader) (*Player, error) { + _, seekable := src.(io.Seeker) + f32Src := convert.NewFloat32BytesReaderFromInt16BytesReader(src) + pi, err := c.playerFactory.newPlayer(c, f32Src, seekable, src, bitDepthInBytesFloat32) + if err != nil { + return nil, err + } + + p := &Player{p: pi} + + p.cleanup = runtime.AddCleanup(p, (*playerImpl).finalize, p.p) + + return p, nil +} + +// NewPlayerF32 creates a new player with the given stream. +// +// src's format must be linear PCM (32bit float, little endian, 2 channel stereo) +// without a header (e.g. RIFF header). +// The sample rate must be same as that of the audio context. +// +// The player is seekable when src is io.Seeker. +// Attempt to seek the player that is not io.Seeker causes panic. +// +// Note that the given src can't be shared with other Player objects. +// +// NewPlayerF32 does not touch src. The player asks src for its current position when it +// first uses src, at [Player.Play] or [Player.SetPosition]. +// +// A Player doesn't close src even if src implements io.Closer. +// Closing the source is src owner's responsibility. +// +// For new code, NewPlayerF32 is preferable to NewPlayer, since Ebitengine will treat only 32bit float audio internally in the future. +// +// A Player for 32bit float must be used with 32bit float version of audio APIs, like vorbis.DecodeF32 or audio.NewInfiniteLoopF32, not vorbis.DecodeWithoutResampling or audio.NewInfiniteLoop. +func (c *Context) NewPlayerF32(src io.Reader) (*Player, error) { + _, seekable := src.(io.Seeker) + pi, err := c.playerFactory.newPlayer(c, src, seekable, src, bitDepthInBytesFloat32) + if err != nil { + return nil, err + } + + p := &Player{p: pi} + p.cleanup = runtime.AddCleanup(p, (*playerImpl).finalize, p.p) + + return p, nil +} + +// NewPlayer creates a new player with the given stream. +// +// Deprecated: as of v2.2. Use (*Context).NewPlayer instead. +func NewPlayer(context *Context, src io.Reader) (*Player, error) { + return context.NewPlayer(src) +} + +// NewPlayerFromBytes creates a new player with the given bytes. +// +// As opposed to NewPlayer, you don't have to care if src is already used by another player or not. +// src can be shared by multiple players. +// +// The format of src should be same as noted at NewPlayer. +func (c *Context) NewPlayerFromBytes(src []byte) *Player { + p, err := c.NewPlayer(bytes.NewReader(src)) + if err != nil { + // Errors should never happen. + panic(fmt.Sprintf("audio: NewPlayerFromBytes failed: %v", err)) + } + return p +} + +// NewPlayerF32FromBytes creates a new player with the given bytes. +// +// As opposed to NewPlayerF32, you don't have to care if src is already used by another player or not. +// src can be shared by multiple players. +// +// The format of src should be same as noted at NewPlayerF32. +func (c *Context) NewPlayerF32FromBytes(src []byte) *Player { + p, err := c.NewPlayerF32(bytes.NewReader(src)) + if err != nil { + // Errors should never happen. + panic(fmt.Sprintf("audio: NewPlayerF32FromBytes failed: %v", err)) + } + return p +} + +// NewPlayerFromBytes creates a new player with the given bytes. +// +// Deprecated: as of v2.2. Use (*Context).NewPlayerFromBytes instead. +func NewPlayerFromBytes(context *Context, src []byte) *Player { + return context.NewPlayerFromBytes(src) +} + +// finalize closes the player when its Player wrapper is no longer reachable. +// +// A Player becomes unreachable as early as the moment one of its methods reads the playerImpl out +// of it, so finalize can run while that method is still in flight. Every Player method keeps its +// receiver alive with runtime.KeepAlive until the delegated call returns, so that finalize does +// not close the player under it. +func (p *playerImpl) finalize() { + if !p.IsPlaying() { + _ = p.Close() + } +} + +// Close closes the stream. +// +// When Close is called, the stream owned by the player is NOT closed, +// even if the stream implements io.Closer. +// +// Close returns error when the player is already closed. +// +// Deprecated: as of v2.10. Use [Player.PauseAndStopReading] instead. +func (p *Player) Close() error { + defer runtime.KeepAlive(p) + return p.p.Close() +} + +// Play plays the stream. +func (p *Player) Play() { + defer runtime.KeepAlive(p) + p.p.Play() +} + +// IsPlaying returns a boolean indicating whether the player is playing. +func (p *Player) IsPlaying() bool { + defer runtime.KeepAlive(p) + return p.p.IsPlaying() +} + +// Rewind rewinds the current position to the start. +// +// The passed source to NewPlayer must be io.Seeker, or Rewind panics. +// Rewind does not panic before Play is called, as it needs no source access +// to rewind a player that has not read the source yet. +// +// Rewind returns error when seeking the source stream returns error. +func (p *Player) Rewind() error { + defer runtime.KeepAlive(p) + return p.p.Rewind() +} + +// SetPosition sets the position with the given offset. +// +// The passed source to NewPlayer must be io.Seeker, or SetPosition panics. +// A zero offset does not panic before Play is called, as it needs no source +// access to reset a player that has not read the source yet. +// +// SetPosition returns error when seeking the source stream returns an error. +// SetPosition returns an error for a negative offset. +func (p *Player) SetPosition(offset time.Duration) error { + defer runtime.KeepAlive(p) + return p.p.SetPosition(offset) +} + +// Seek seeks the position with the given offset. +// +// Deprecated: as of v2.6. Use SetPosition instead. +func (p *Player) Seek(offset time.Duration) error { + return p.SetPosition(offset) +} + +// Pause pauses the playing. +func (p *Player) Pause() { + defer runtime.KeepAlive(p) + p.p.Pause() +} + +// PauseAndStopReading pauses the playing and stops reading the source. +// After PauseAndStopReading returns, this player does not read the source until [Player.Play], +// [Player.SetPosition], or [Player.Rewind] is called, so the source can be closed safely. +// The buffered data is kept, and [Player.Play] resumes the playing without a gap. +// PauseAndStopReading blocks until an ongoing read from the source finishes, if any. +func (p *Player) PauseAndStopReading() { + defer runtime.KeepAlive(p) + p.p.PauseAndStopReading() +} + +// Position returns the current position in time. +// +// As long as the player continues to play, Position's returning value is increased monotonically, +// even though the source stream loops and its position goes back. +func (p *Player) Position() time.Duration { + defer runtime.KeepAlive(p) + return p.p.Position() +} + +// Current returns the current position in time. +// +// Deprecated: as of v2.6. Use Position instead. +func (p *Player) Current() time.Duration { + return p.Position() +} + +// Volume returns the current volume of this player, which is 0 or larger. +func (p *Player) Volume() float64 { + defer runtime.KeepAlive(p) + return p.p.Volume() +} + +// SetVolume sets the volume of this player, which must be in the range of [0, math.MaxFloat32]. +// A volume larger than 1 amplifies the sound and might cause clipping. +// A value out of the range, including NaN, is treated as 0. +func (p *Player) SetVolume(volume float64) { + defer runtime.KeepAlive(p) + p.p.SetVolume(volume) +} + +// SetBufferSize adjusts the buffer size of the player. +// If a nonpositive value or a value too large to represent is specified, the default buffer size is used. +// A small buffer size is useful if you want to play a real-time PCM for example. +// Note that the audio quality might be affected if you modify the buffer size. +func (p *Player) SetBufferSize(bufferSize time.Duration) { + defer runtime.KeepAlive(p) + p.p.SetBufferSize(bufferSize) +} + +type hooker interface { + OnSuspendAudio(f func() error) + OnResumeAudio(f func() error) + AppendHookOnBeforeUpdateWithVMGuestInfo(f func(vmGuest bool) error) +} + +var hookerForTesting hooker + +func getHook() hooker { + if hookerForTesting != nil { + return hookerForTesting + } + return &hookerImpl{} +} + +type hookerImpl struct{} + +func (h *hookerImpl) OnSuspendAudio(f func() error) { + hook.OnSuspendAudio(f) +} + +func (h *hookerImpl) OnResumeAudio(f func() error) { + hook.OnResumeAudio(f) +} + +func (h *hookerImpl) AppendHookOnBeforeUpdateWithVMGuestInfo(f func(vmGuest bool) error) { + hook.AppendHookOnBeforeUpdateWithVMGuestInfo(f) +} + +// ResampleReader converts the sample rate of the given signed 16bit integer, little-endian, 2 channels (stereo) stream. +// length is the length of the source stream in bytes. 0 indicates the length is unknown. +// from is the original sample rate. +// to is the target sample rate. +// ResampleReader panics if from or to is not positive. +// +// If the source ends before length bytes, the remainder of the result is silence. +// +// If the original sample rate equals the new one, ResampleReader returns source as it is and does not apply length. +// When the rates differ, the returned value's Read returns only whole samples (4 bytes each), and its Seek rounds a position in +// the middle of a sample down to a sample boundary. +// For a non-empty buffer shorter than one sample, Read returns [io.ErrShortBuffer], or [io.EOF] once the stream has ended. +// +// The returned value implements io.Seeker when the source implements io.Seeker. +// The returned value might implement io.Seeker even when the source doesn't implement io.Seeker, but +// there is no guarantee that the Seek function works correctly. +func ResampleReader(source io.Reader, length int64, from, to int) io.Reader { + validateResamplingSampleRates(from, to) + if from == to { + return source + } + return convert.NewResampling(source, resamplingSourceLength(length), from, to, bitDepthInBytesInt16) +} + +// ResampleReaderF32 converts the sample rate of the given 32bit float, little-endian, 2 channels (stereo) stream. +// length is the length of the source stream in bytes. 0 indicates the length is unknown. +// from is the original sample rate. +// to is the target sample rate. +// ResampleReaderF32 panics if from or to is not positive. +// +// If the source ends before length bytes, the remainder of the result is silence. +// +// If the original sample rate equals the new one, ResampleReaderF32 returns source as it is and does not apply length. +// When the rates differ, the returned value's Read returns only whole samples (8 bytes each), and its Seek rounds a position in +// the middle of a sample down to a sample boundary. +// For a non-empty buffer shorter than one sample, Read returns [io.ErrShortBuffer], or [io.EOF] once the stream has ended. +// +// The returned value implements io.Seeker when the source implements io.Seeker. +// The returned value might implement io.Seeker even when the source doesn't implement io.Seeker, but +// there is no guarantee that the Seek function works correctly. +func ResampleReaderF32(source io.Reader, length int64, from, to int) io.Reader { + validateResamplingSampleRates(from, to) + if from == to { + return source + } + return convert.NewResampling(source, resamplingSourceLength(length), from, to, bitDepthInBytesFloat32) +} + +// Resample converts the sample rate of the given signed 16bit integer, little-endian, 2 channels (stereo) stream. +// length is the length of the source stream in bytes. 0 indicates the length is unknown. +// from is the original sample rate. +// to is the target sample rate. +// Resample panics if from or to is not positive. +// +// If the source ends before length bytes, the remainder of the result is silence. +// +// If the original sample rate equals the new one, Resample returns source as it is and does not apply length. +// When the rates differ, the returned value's Read returns only whole samples (4 bytes each), and its Seek rounds a position in +// the middle of a sample down to a sample boundary. +// For a non-empty buffer shorter than one sample, Read returns [io.ErrShortBuffer], or [io.EOF] once the stream has ended. +// +// Deprecated: as of v2.9. Use ResampleReader instead. +func Resample(source io.ReadSeeker, length int64, from, to int) io.ReadSeeker { + validateResamplingSampleRates(from, to) + if from == to { + return source + } + return convert.NewResampling(source, resamplingSourceLength(length), from, to, bitDepthInBytesInt16) +} + +// ResampleF32 converts the sample rate of the given 32bit float, little-endian, 2 channels (stereo) stream. +// length is the length of the source stream in bytes. 0 indicates the length is unknown. +// from is the original sample rate. +// to is the target sample rate. +// ResampleF32 panics if from or to is not positive. +// +// If the source ends before length bytes, the remainder of the result is silence. +// +// If the original sample rate equals the new one, ResampleF32 returns source as it is and does not apply length. +// When the rates differ, the returned value's Read returns only whole samples (8 bytes each), and its Seek rounds a position in +// the middle of a sample down to a sample boundary. +// For a non-empty buffer shorter than one sample, Read returns [io.ErrShortBuffer], or [io.EOF] once the stream has ended. +// +// Deprecated: as of v2.9. Use ResampleReaderF32 instead. +func ResampleF32(source io.ReadSeeker, length int64, from, to int) io.ReadSeeker { + validateResamplingSampleRates(from, to) + if from == to { + return source + } + return convert.NewResampling(source, resamplingSourceLength(length), from, to, bitDepthInBytesFloat32) +} + +// resamplingSourceLength converts a length given to ResampleReader, ResampleReaderF32, Resample, or ResampleF32, +// where 0 indicates an unknown length, to a length for convert.NewResampling, +// where a negative value indicates an unknown length and 0 an empty stream. +// +// TODO: Let these functions take a negative value instead of 0 for an unknown length (#3395). +func resamplingSourceLength(length int64) int64 { + if length == 0 { + return -1 + } + return length +} + +func validateResamplingSampleRates(from, to int) { + if from <= 0 { + panic(fmt.Sprintf("audio: original sample rate must be positive but was %d", from)) + } + if to <= 0 { + panic(fmt.Sprintf("audio: target sample rate must be positive but was %d", to)) + } +} diff --git a/audio/audio_test.go b/audio/audio_test.go new file mode 100644 index 0000000..f0ab5c8 --- /dev/null +++ b/audio/audio_test.go @@ -0,0 +1,997 @@ +// 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_test + +import ( + "bytes" + "errors" + "io" + "math" + "runtime" + "strconv" + "sync" + "sync/atomic" + "testing" + "time" + + "github.com/hajimehoshi/ebiten/v2/audio" +) + +var context *audio.Context + +func setup() { + context = audio.NewContext(44100) +} + +func teardown() { + audio.ResetContextForTesting() + context = nil +} + +// Issue #746 +func TestGC(t *testing.T) { + setup() + defer teardown() + + p, _ := context.NewPlayer(bytes.NewReader(make([]byte, 4))) + got := audio.PlayersCountForTesting() + if want := 0; got != want { + t.Errorf("PlayersCountForTesting(): got: %d, want: %d", got, want) + } + + p.Play() + got = audio.PlayersCountForTesting() + if want := 1; got != want { + t.Errorf("PlayersCountForTesting() after Play: got: %d, want: %d", got, want) + } + + runtime.KeepAlive(p) + p = nil + runtime.GC() + + for range 10 { + got = audio.PlayersCountForTesting() + if want := 0; got == want { + return + } + if err := audio.UpdateForTesting(); err != nil { + t.Error(err) + } + // 200[ms] should be enough for all the bytes to be consumed. + // TODO: This is a dirty hack. Would it be possible to use virtual time? + time.Sleep(200 * time.Millisecond) + } + t.Errorf("time out") +} + +type infiniteReader struct{} + +func (i *infiniteReader) Read(p []byte) (int, error) { + for i := range p { + p[i] = 0 + } + return len(p), nil +} + +// Issue #853 +func TestSameSourcePlayers(t *testing.T) { + setup() + defer teardown() + + src := &infiniteReader{} + p0, err := context.NewPlayer(src) + if err != nil { + t.Fatal(err) + } + p1, err := context.NewPlayer(src) + if err != nil { + t.Fatal(err) + } + + // As the player does not play yet, error doesn't happen. + if err := audio.UpdateForTesting(); err != nil { + t.Error(err) + } + + p0.Play() + p1.Play() + + for range 10 { + if err := audio.UpdateForTesting(); err != nil { + // An error is expected. + return + } + time.Sleep(200 * time.Millisecond) + } + t.Errorf("time out") +} + +func TestPauseBeforeInit(t *testing.T) { + setup() + defer teardown() + + src := bytes.NewReader(make([]byte, 4)) + p, err := context.NewPlayer(src) + if err != nil { + t.Fatal(err) + } + + p.Play() + p.Pause() + p.Play() + + if err := audio.UpdateForTesting(); err != nil { + t.Error(err) + } +} + +type emptySource struct{} + +func (emptySource) Read(buf []byte) (int, error) { + return len(buf), nil +} + +func TestNonSeekableSource(t *testing.T) { + if runtime.GOOS == "js" { + t.Skip("infinite streams in tests cannot be treated well on browsers") + } + + setup() + defer teardown() + + p, err := context.NewPlayer(emptySource{}) + if err != nil { + t.Fatal(err) + } + + p.Play() + p.Pause() + + if err := audio.UpdateForTesting(); err != nil { + t.Error(err) + } +} + +// Issue #3438 +func TestDeferredDeviceCreation(t *testing.T) { + setup() + defer teardown() + + p, err := context.NewPlayer(bytes.NewReader(make([]byte, 4))) + if err != nil { + t.Fatal(err) + } + + // Touching a player before the first update must not create the audio device. + p.SetVolume(0.5) + p.Play() + + if audio.ContextCreatedForTesting() { + t.Errorf("the audio device must not be created before the first update") + } + + // The state set before the device is created is recorded. + if got, want := p.Volume(), 0.5; got != want { + t.Errorf("Volume(): got: %v, want: %v", got, want) + } + if got, want := p.IsPlaying(), true; got != want { + t.Errorf("IsPlaying(): got: %t, want: %t", got, want) + } + if got, want := audio.PlayersCountForTesting(), 1; got != want { + t.Errorf("PlayersCountForTesting(): got: %d, want: %d", got, want) + } + + // The first update creates the audio device. + if err := audio.UpdateForTesting(); err != nil { + t.Error(err) + } + if !audio.ContextCreatedForTesting() { + t.Errorf("the audio device must be created after the first update") + } + + // The pending player eventually starts playing and finishes, which is only possible + // once the device is created and the player is materialized. + for range 10 { + if audio.PlayersCountForTesting() == 0 { + return + } + if err := audio.UpdateForTesting(); err != nil { + t.Error(err) + } + time.Sleep(200 * time.Millisecond) + } + t.Errorf("time out") +} + +// Issue #3438 +func TestSetPositionBeforeDeviceCreation(t *testing.T) { + setup() + defer teardown() + + // 44100 [Hz] * 8 [bytes/sample] is one second of 32bit float stereo audio. + src := bytes.NewReader(make([]byte, 44100*8)) + p, err := context.NewPlayerF32(src) + if err != nil { + t.Fatal(err) + } + + if err := p.SetPosition(500 * time.Millisecond); err != nil { + t.Fatal(err) + } + + // SetPosition must not create the audio device. + if audio.ContextCreatedForTesting() { + t.Errorf("the audio device must not be created before the first update") + } + + // The position set before the device is created is recorded. + if got, want := p.Position(), 500*time.Millisecond; got != want { + t.Errorf("Position(): got: %v, want: %v", got, want) + } +} + +func TestSetPositionLongDuration(t *testing.T) { + setup() + defer teardown() + + p, err := context.NewPlayerF32(bytes.NewReader(nil)) + if err != nil { + t.Fatal(err) + } + + // 10 milliseconds is exactly 441 samples at 44100 Hz, so the position + // round-trips exactly while exercising both the whole-seconds and the + // remainder terms of the conversion. + const duration = 8*time.Hour + 10*time.Millisecond + if err := p.SetPosition(duration); err != nil { + t.Fatal(err) + } + if got, want := p.Position(), duration; got != want { + t.Errorf("Position(): got: %v, want: %v", got, want) + } +} + +func TestSetPositionNegative(t *testing.T) { + tests := []struct { + name string + createDevice bool + }{ + { + name: "BeforeDeviceCreation", + createDevice: false, + }, + { + name: "AfterDeviceCreation", + createDevice: true, + }, + } + offsets := []time.Duration{ + -time.Nanosecond, + -10 * time.Microsecond, + -30 * time.Microsecond, + -time.Second, + } + for _, test := range tests { + for _, offset := range offsets { + t.Run(test.name+"/"+offset.String(), func(t *testing.T) { + setup() + defer teardown() + + src := bytes.NewReader(make([]byte, 44100*8)) + p, err := context.NewPlayerF32(src) + if err != nil { + t.Fatal(err) + } + defer func() { + if err := p.Close(); err != nil { + t.Errorf("Close: %v", err) + } + }() + if err := p.SetPosition(500 * time.Millisecond); err != nil { + t.Fatal(err) + } + if test.createDevice { + if err := audio.UpdateForTesting(); err != nil { + t.Fatal(err) + } + // Keep the player paused so that background updates cannot change its position. + if err := audio.EnsurePlayerForTesting(p); err != nil { + t.Fatal(err) + } + } + + const want = 500 * time.Millisecond + if err := p.SetPosition(offset); err == nil { + t.Errorf("SetPosition(%v): got: nil, want: an error", offset) + } + if got := p.Position(); got != want { + t.Errorf("Position() after SetPosition(%v): got: %v, want: %v", offset, got, want) + } + }) + } + } +} + +// Issue #3438 +func TestRewindNonSeekableBeforeDeviceCreation(t *testing.T) { + setup() + defer teardown() + + p, err := context.NewPlayer(emptySource{}) + if err != nil { + t.Fatal(err) + } + + p.Play() + + // Rewinding to the start before the device is created must not panic even for a + // non-seekable source, as the stream has not been read yet. + if err := p.Rewind(); err != nil { + t.Fatal(err) + } + + p.Pause() + + if err := audio.UpdateForTesting(); err != nil { + t.Error(err) + } +} + +func TestSameSourceForMultiplePlayers(t *testing.T) { + setup() + defer teardown() + + src := bytes.NewReader(make([]byte, 4)) + + p1, err := context.NewPlayer(src) + if err != nil { + t.Fatal(err) + } + p1.Play() + + p2, err := context.NewPlayer(src) + if err != nil { + t.Fatal(err) + } + p2.Play() + + p1.Pause() + p2.Pause() + + if err := audio.UpdateForTesting(); err == nil { + t.Error("UpdateForTesting() must return an error, but did not") + } +} + +type uncomparableSource []int + +func (uncomparableSource) Read(buf []byte) (int, error) { + return 0, io.EOF +} + +// Issue #3039 +func TestUncomparableSource(t *testing.T) { + setup() + defer teardown() + + p, err := context.NewPlayer(uncomparableSource{}) + if err != nil { + t.Fatal(err) + } + + p.Play() + p.Pause() + + if err := audio.UpdateForTesting(); err != nil { + t.Error(err) + } +} + +func TestComparableSourceAfterUncomparable(t *testing.T) { + setup() + defer teardown() + + p1, err := context.NewPlayer(uncomparableSource{}) + if err != nil { + t.Fatal(err) + } + p1.Play() + + p2, err := context.NewPlayer(bytes.NewReader(make([]byte, 4))) + if err != nil { + t.Fatal(err) + } + p2.Play() + + p1.Pause() + p2.Pause() +} + +// countingSource is an infinite source counting the Read calls. +type countingSource struct { + reads atomic.Int64 +} + +func (s *countingSource) Read(buf []byte) (int, error) { + s.reads.Add(1) + return len(buf), nil +} + +// waitForRead waits until src's read count differs from reads and returns the new count. +func waitForRead(t *testing.T, src *countingSource, reads int64) int64 { + t.Helper() + deadline := time.Now().Add(time.Second) + for { + if got := src.reads.Load(); got != reads { + return got + } + if time.Now().After(deadline) { + t.Fatal("the source was not read") + } + time.Sleep(time.Millisecond) + } +} + +// Issue #3510 +func TestPauseAndStopReading(t *testing.T) { + if runtime.GOOS == "js" { + t.Skip("infinite streams in tests cannot be treated well on browsers") + } + + setup() + defer teardown() + + src := &countingSource{} + p, err := context.NewPlayerF32(src) + if err != nil { + t.Fatal(err) + } + + p.Play() + + // The first update creates the audio device and starts the pending player. + if err := audio.UpdateForTesting(); err != nil { + t.Error(err) + } + + waitForRead(t, src, 0) + + p.PauseAndStopReading() + + if p.IsPlaying() { + t.Error("IsPlaying() after PauseAndStopReading: got: true, want: false") + } + if got, want := audio.PlayersCountForTesting(), 0; got != want { + t.Errorf("PlayersCountForTesting() after PauseAndStopReading: got: %d, want: %d", got, want) + } + + // After PauseAndStopReading, the source must not be read. + reads := src.reads.Load() + time.Sleep(100 * time.Millisecond) + if got := src.reads.Load(); got != reads { + t.Errorf("the source was read after PauseAndStopReading: got: %d reads, want: %d", got, reads) + } + + // The player is reusable after PauseAndStopReading. + p.Play() + waitForRead(t, src, reads) +} + +// Issue #3510 +func TestPauseAndStopReadingWhilePaused(t *testing.T) { + if runtime.GOOS == "js" { + t.Skip("infinite streams in tests cannot be treated well on browsers") + } + + setup() + defer teardown() + + src := &countingSource{} + p, err := context.NewPlayerF32(src) + if err != nil { + t.Fatal(err) + } + + p.Play() + + // The first update creates the audio device and starts the pending player. + if err := audio.UpdateForTesting(); err != nil { + t.Error(err) + } + + reads := waitForRead(t, src, 0) + + // A paused player keeps reading the source to fill its buffer. + p.Pause() + waitForRead(t, src, reads) + + // PauseAndStopReading must stop the reads even though the player is already paused. + p.PauseAndStopReading() + reads = src.reads.Load() + time.Sleep(100 * time.Millisecond) + if got := src.reads.Load(); got != reads { + t.Errorf("the source was read after PauseAndStopReading: got: %d reads, want: %d", got, reads) + } +} + +// Issue #3510 +func TestPauseAndStopReadingBeforeInit(t *testing.T) { + setup() + defer teardown() + + p, err := context.NewPlayer(bytes.NewReader(make([]byte, 4))) + if err != nil { + t.Fatal(err) + } + + // PauseAndStopReading before the device is created cancels the pending play. + p.Play() + p.PauseAndStopReading() + + if p.IsPlaying() { + t.Error("IsPlaying() after PauseAndStopReading: got: true, want: false") + } + if got, want := audio.PlayersCountForTesting(), 0; got != want { + t.Errorf("PlayersCountForTesting() after PauseAndStopReading: got: %d, want: %d", got, want) + } + + if err := audio.UpdateForTesting(); err != nil { + t.Error(err) + } +} + +// Issue #3359 +func TestSetVolumeInvalidValue(t *testing.T) { + setup() + defer teardown() + + p, err := context.NewPlayer(&infiniteReader{}) + if err != nil { + t.Fatal(err) + } + + volumes := []struct { + in float64 + want float64 + }{ + {in: 0.5, want: 0.5}, + {in: 2, want: 2}, + {in: math.MaxFloat32, want: math.MaxFloat32}, + {in: -1, want: 0}, + {in: math.MaxFloat32 * 2, want: 0}, + {in: math.Inf(1), want: 0}, + {in: math.Inf(-1), want: 0}, + {in: math.NaN(), want: 0}, + } + + // The volume is kept by the player itself before the audio device is created. + for _, v := range volumes { + p.SetVolume(v.in) + if got, want := p.Volume(), v.want; got != want { + t.Errorf("Volume() after SetVolume(%v) before the device creation: got: %v, want: %v", v.in, got, want) + } + } + + p.Play() + if err := audio.UpdateForTesting(); err != nil { + t.Fatal(err) + } + if !audio.ContextCreatedForTesting() { + t.Fatal("the audio device must be created after the first update") + } + + // The volume is kept by the underlying player after the audio device is created. + for _, v := range volumes { + p.SetVolume(v.in) + if got, want := p.Volume(), v.want; got != want { + t.Errorf("Volume() after SetVolume(%v) after the device creation: got: %v, want: %v", v.in, got, want) + } + } +} + +// Issue #3655 +func TestSetBufferSize(t *testing.T) { + setup() + defer teardown() + + p, err := context.NewPlayer(&infiniteReader{}) + if err != nil { + t.Fatal(err) + } + + var largeBufferSize int + if strconv.IntSize == 64 { + largeBufferSize64 := int64(19_051_200_000) + largeBufferSize = int(largeBufferSize64) + } + tests := []struct { + in time.Duration + want int + }{ + {in: -time.Second, want: 0}, + {in: 0, want: 0}, + {in: 50 * time.Millisecond, want: 17640}, + {in: 15 * time.Hour, want: largeBufferSize}, + } + + for _, test := range tests { + p.SetBufferSize(test.in) + if got := audio.BufferSizeForTesting(p); got != test.want { + t.Errorf("buffer size for %v: got: %d, want: %d", test.in, got, test.want) + } + } +} + +func TestPositionNotGrowingAfterFinished(t *testing.T) { + setup() + defer teardown() + + // 44100 [Hz] * 8 [bytes/sample] is one second of 32bit float stereo audio. + src := bytes.NewReader(make([]byte, 44100*8*4)) + p, err := context.NewPlayerF32(src) + if err != nil { + t.Fatal(err) + } + + p.Play() + // Wait until the player finishes its source. The deadline is generous because time.Sleep has + // a several-millisecond floor on browsers, which slows draining the source down. + var finished bool + deadline := time.Now().Add(10 * time.Second) + for time.Now().Before(deadline) { + if err := audio.UpdateForTesting(); err != nil { + t.Fatal(err) + } + if !p.IsPlaying() { + finished = true + break + } + time.Sleep(5 * time.Millisecond) + } + if !finished { + t.Fatal("time out: the player did not finish") + } + + end := p.Position() + if got, want := end, 4*time.Second; got < want { + t.Errorf("Position() after the player finished: got: %v, want: at least %v", got, want) + } + + // Play on a finished player does nothing, so the position must not grow anymore. + for range 20 { + p.Play() + if err := audio.UpdateForTesting(); err != nil { + t.Fatal(err) + } + time.Sleep(5 * time.Millisecond) + } + if got := p.Position(); got != end { + t.Errorf("position grew after the player finished: %v -> %v", end, got) + } +} + +func TestNewPlayerWithSourceOfClosedPlayer(t *testing.T) { + setup() + defer teardown() + + src := bytes.NewReader(make([]byte, 256)) + + p0, err := context.NewPlayer(src) + if err != nil { + t.Fatal(err) + } + p0.Play() + if err := audio.UpdateForTesting(); err != nil { + t.Fatal(err) + } + if err := p0.Close(); err != nil { + t.Fatal(err) + } + + // A closed player no longer owns its source, so a new player can take the source over + // immediately. + p1, err := context.NewPlayer(src) + if err != nil { + t.Fatal(err) + } + p1.Play() + if err := audio.UpdateForTesting(); err != nil { + t.Errorf("UpdateForTesting after a new player took the source of a closed player over: %v", err) + } +} + +func TestRestartWhilePlayersAreSwept(t *testing.T) { + if runtime.GOOS == "js" { + t.Skip("goroutines cannot run in parallel on browsers") + } + + setup() + defer teardown() + + // The first update creates the audio device, so that the players below play for real. + if err := audio.UpdateForTesting(); err != nil { + t.Fatal(err) + } + + // Many players widen the window between the sweep's playing check and its removal, as the + // removal used to happen only after the whole list was checked. + const playerCount = 64 + + players := make([]*audio.Player, 0, playerCount) + for range playerCount { + // A short source finishes quickly, so that the restart pattern below runs many times. + p, err := context.NewPlayerF32(bytes.NewReader(make([]byte, 4096))) + if err != nil { + t.Fatal(err) + } + p.Play() + players = append(players, p) + } + + var untracked atomic.Bool + var wg sync.WaitGroup + deadline := time.Now().Add(2 * time.Second) + for _, p := range players { + wg.Go(func() { + for time.Now().Before(deadline) && !untracked.Load() { + // The typical pattern to replay a finished sound effect every tick. + if !p.IsPlaying() { + if err := p.Rewind(); err != nil { + return + } + p.Play() + } + if audio.PlayingButUntrackedForTesting(p) { + untracked.Store(true) + return + } + } + }) + } + wg.Wait() + + if untracked.Load() { + t.Error("a playing player was removed from the context") + } + + // The check above is meaningful only if the players finished and restarted normally, without + // any error being reported. + if err := audio.UpdateForTesting(); err != nil { + t.Fatal(err) + } +} + +// errSourceFailed is the error a failingSource returns once it has run out of data. +var errSourceFailed = errors.New("audio_test: source failed") + +// failingSource is a source which returns data for a while and then fails with an error other +// than io.EOF, like a corrupted asset or a broken network stream. +type failingSource struct { + remaining int +} + +func (s *failingSource) Read(buf []byte) (int, error) { + if s.remaining <= 0 { + return 0, errSourceFailed + } + n := min(len(buf), s.remaining) + s.remaining -= n + return n, nil +} + +// Issue #3647 +func TestPlayerErrorDoesNotStopOtherPlayers(t *testing.T) { + if runtime.GOOS == "js" { + t.Skip("infinite streams in tests cannot be treated well on browsers") + } + + setup() + defer teardown() + + // The first update creates the audio device, so that the players below play for real. + if err := audio.UpdateForTesting(); err != nil { + t.Fatal(err) + } + + failing, err := context.NewPlayerF32(&failingSource{remaining: 4096 * 4}) + if err != nil { + t.Fatal(err) + } + // 44100 [Hz] * 8 [bytes/sample] is one second of 32bit float stereo audio. + healthy, err := context.NewPlayerF32(audio.NewInfiniteLoopF32(bytes.NewReader(make([]byte, 44100*8)), 44100*8)) + if err != nil { + t.Fatal(err) + } + + failing.Play() + healthy.Play() + + // Wait until the failing player's source fails and the failure is reported as the context + // error. + var failed bool + deadline := time.Now().Add(5 * time.Second) + for time.Now().Before(deadline) { + if err := audio.UpdateForTesting(); err != nil { + if !errors.Is(err, errSourceFailed) { + t.Fatal(err) + } + failed = true + break + } + time.Sleep(5 * time.Millisecond) + } + if !failed { + t.Fatal("time out: the failing player's error was not reported") + } + + // The failed player is no longer tracked as playing, so that its error is reported once and + // the healthy player is the only playing player left. + deadline = time.Now().Add(time.Second) + for audio.PlayersCountForTesting() != 1 && time.Now().Before(deadline) { + time.Sleep(5 * time.Millisecond) + } + if got, want := audio.PlayersCountForTesting(), 1; got != want { + t.Errorf("PlayersCountForTesting() after a player failed: got: %d, want: %d", got, want) + } + + // The healthy player must keep playing and its position must keep advancing: one player's + // failure must not stop the context from updating the other players. + if !healthy.IsPlaying() { + t.Error("IsPlaying() of the healthy player after another player failed: got: false, want: true") + } + pos0 := healthy.Position() + time.Sleep(100 * time.Millisecond) + if pos1 := healthy.Position(); pos1 <= pos0 { + t.Errorf("Position() of the healthy player did not advance after another player failed: %v -> %v", pos0, pos1) + } + + // The context error is sticky. + if err := audio.UpdateForTesting(); !errors.Is(err, errSourceFailed) { + t.Errorf("UpdateForTesting() after a player failed: got: %v, want: %v", err, errSourceFailed) + } +} + +func TestPlayOnIdleContext(t *testing.T) { + setup() + defer teardown() + + // The first update creates the audio device, so that the players below play for real. + if err := audio.UpdateForTesting(); err != nil { + t.Fatal(err) + } + + // waitUntil polls cond until it holds or the deadline passes, and reports whether it held. + // The deadline is generous because time.Sleep has a several-millisecond floor on browsers. + waitUntil := func(cond func() bool) bool { + deadline := time.Now().Add(10 * time.Second) + for time.Now().Before(deadline) { + if cond() { + return true + } + time.Sleep(5 * time.Millisecond) + } + return false + } + + // A player's position is advanced only by the context, so a growing position shows that the + // context updates the player. The second round starts a player after the context has dropped + // the finished player of the first round, that is, after it went idle by itself. + for i := range 2 { + // Let the context settle into idling with no player to update. + time.Sleep(50 * time.Millisecond) + + // 44100 [Hz] * 4 [bytes/sample] is one second of 16bit stereo audio. + p, err := context.NewPlayer(bytes.NewReader(make([]byte, 44100*4))) + if err != nil { + t.Fatal(err) + } + p.Play() + + if !waitUntil(func() bool { return p.Position() > 0 }) { + t.Errorf("round %d: Position() did not advance after Play on an idle context", i) + return + } + + // The next round needs the context to be idle again, which is only possible once the + // context has dropped the finished player. + if !waitUntil(func() bool { return audio.PlayersCountForTesting() == 0 }) { + t.Fatalf("round %d: time out: the player did not finish", i) + } + } +} + +func callWithTimeout(t *testing.T, name string, f func() error) error { + t.Helper() + done := make(chan error, 1) + go func() { + done <- f() + }() + select { + case err := <-done: + return err + case <-time.After(5 * time.Second): + t.Fatalf("%s did not return", name) + } + return nil +} + +func runWithTimeout(t *testing.T, name string, f func() error) { + t.Helper() + if err := callWithTimeout(t, name, f); err != nil { + t.Errorf("%s: %v", name, err) + } +} + +var errDeviceFailed = errors.New("audio_test: device failed") + +func TestSuspendRetriesAfterError(t *testing.T) { + setup() + defer teardown() + + if err := audio.UpdateForTesting(); err != nil { + t.Fatal(err) + } + + audio.SetSuspendErrorForTesting(errDeviceFailed) + defer audio.SetSuspendErrorForTesting(nil) + if err := callWithTimeout(t, "suspend", audio.SuspendForTesting); !errors.Is(err, errDeviceFailed) { + t.Errorf("suspend: got: %v, want: %v", err, errDeviceFailed) + } + + audio.SetSuspendErrorForTesting(nil) + runWithTimeout(t, "suspend", audio.SuspendForTesting) + runWithTimeout(t, "resume", audio.ResumeForTesting) + runWithTimeout(t, "suspend", audio.SuspendForTesting) + runWithTimeout(t, "resume", audio.ResumeForTesting) +} + +func TestResumeRetriesAfterError(t *testing.T) { + setup() + defer teardown() + + if err := audio.UpdateForTesting(); err != nil { + t.Fatal(err) + } + runWithTimeout(t, "suspend", audio.SuspendForTesting) + + audio.SetResumeErrorForTesting(errDeviceFailed) + defer audio.SetResumeErrorForTesting(nil) + if err := callWithTimeout(t, "resume", audio.ResumeForTesting); !errors.Is(err, errDeviceFailed) { + t.Errorf("resume: got: %v, want: %v", err, errDeviceFailed) + } + + audio.SetResumeErrorForTesting(nil) + runWithTimeout(t, "resume", audio.ResumeForTesting) + runWithTimeout(t, "suspend", audio.SuspendForTesting) + runWithTimeout(t, "resume", audio.ResumeForTesting) +} + +func TestTimeStreamSeekCurrentOverInfiniteLoop(t *testing.T) { + const length = 64 + s, err := audio.NewTimeStreamForTesting(audio.NewInfiniteLoop(bytes.NewReader(make([]byte, length)), length), 48000) + if err != nil { + t.Fatal(err) + } + if _, err := io.ReadFull(s, make([]byte, length+16)); err != nil { + t.Fatal(err) + } + pos, err := s.Seek(0, io.SeekCurrent) + if err != nil { + t.Fatal(err) + } + if want := int64(length + 16); pos != want { + t.Errorf("Seek(0, io.SeekCurrent): got %d, want %d", pos, want) + } +} diff --git a/audio/export_test.go b/audio/export_test.go new file mode 100644 index 0000000..654df73 --- /dev/null +++ b/audio/export_test.go @@ -0,0 +1,341 @@ +// 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) +}