TestGC, TestSameSourcePlayers, TestDeferredDeviceCreation, TestPauseAndStopReading, TestPauseAndStopReadingWhilePaused, TestPositionNotGrowingAfterFinished and TestPlayerErrorDoesNotStopOtherPlayers now run in a synctest bubble, so their sleeps and deadlines take no real time. A bubble must not leave goroutines behind, so ResetContextForTesting now stops the goroutine updating the players and the dummy players reading their sources, and waits for them to exit. Before, every test leaked them. Updates #3716
756 lines
26 KiB
Go
756 lines
26 KiB
Go
// 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 or when the context is
|
|
// disposed. The goroutine updating the players waits on it while playingPlayers is empty.
|
|
cond *sync.Cond
|
|
|
|
// disposed reports that the goroutine updating the players must exit. A context lives until the
|
|
// process exits, so this is set only when a test discards the context.
|
|
disposed bool
|
|
|
|
// updater tracks the goroutine updating the players, so that a test discarding the context can wait
|
|
// for the goroutine to exit.
|
|
updater sync.WaitGroup
|
|
}
|
|
|
|
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.
|
|
c.updater.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.
|
|
if !c.waitForPlayingPlayers() {
|
|
return
|
|
}
|
|
// 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, and reports
|
|
// whether the players should be updated. It returns false once the context is disposed.
|
|
func (c *Context) waitForPlayingPlayers() bool {
|
|
c.m.Lock()
|
|
defer c.m.Unlock()
|
|
for len(c.playingPlayers) == 0 && !c.disposed {
|
|
c.cond.Wait()
|
|
}
|
|
return !c.disposed
|
|
}
|
|
|
|
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))
|
|
}
|
|
}
|