Use SoundBuffer for memory sounds

This commit is contained in:
bloeys
2022-06-25 20:13:13 +04:00
parent 6c166db5ef
commit 2e3fb46bcb
4 changed files with 176 additions and 62 deletions

70
sound_buffer.go Executable file
View File

@ -0,0 +1,70 @@
package wavy
import (
"errors"
"io"
)
//Pre-defined errors
var (
ErrInvalidWhence = errors.New("invalid whence value. Must be: io.SeekStart, io.SeekCurrent, or io.SeekEnd")
ErrNegativeSeekPos = errors.New("negative seeker position")
)
var _ io.ReadSeeker = &SoundBuffer{}
type SoundBuffer struct {
Data []byte
//Pos is the starting position of the next read
Pos int64
}
//Read only returns io.EOF when no more input is available
func (sb *SoundBuffer) Read(outBuf []byte) (bytesRead int, err error) {
bytesRead = copy(outBuf, sb.Data[sb.Pos:])
if bytesRead == 0 {
return 0, io.EOF
}
sb.Pos += int64(bytesRead)
return bytesRead, nil
}
//Seek returns the new position.
//An error is only returned if the whence is invalid or if the resulting position is negative.
//
//If the resulting position is >=len(SoundBuffer.Data) then future Read() calls will return io.EOF
func (sb *SoundBuffer) Seek(offset int64, whence int) (int64, error) {
newPos := sb.Pos
switch whence {
case io.SeekStart:
newPos = offset
case io.SeekCurrent:
newPos += offset
case io.SeekEnd:
newPos = int64(len(sb.Data)) + offset
default:
return 0, ErrInvalidWhence
}
if newPos < 0 {
return 0, ErrNegativeSeekPos
}
sb.Pos = newPos
return sb.Pos, nil
}
//Clone returns a new SoundBuffer that uses the same SoundBuffer.Data but with an independent ReadSeeker.
//Allows you to have many readers all reading from different positions of the same buffer.
//
//The new buffer starts reading from the start (Pos=0)
func (sb *SoundBuffer) Clone() SoundBuffer {
return SoundBuffer{
Data: sb.Data,
Pos: 0,
}
}

36
sound_enums.go Executable file
View File

@ -0,0 +1,36 @@
package wavy
type SoundType int
const (
SoundType_Unknown SoundType = iota
SoundType_MP3
)
type SampleRate int
const (
SampleRate_44100 SampleRate = 44100
SampleRate_48000 SampleRate = 48000
)
type SoundChannelCount int
const (
SoundChannelCount_1 SoundChannelCount = 1
SoundChannelCount_2 SoundChannelCount = 2
)
type SoundBitDepth int
const (
SoundBitDepth_1 SoundBitDepth = 1
SoundBitDepth_2 SoundBitDepth = 2
)
type SoundMode int
const (
SoundMode_Streaming SoundMode = iota
SoundMode_Memory
)

129
wavy.go
View File

@ -14,49 +14,40 @@ import (
"github.com/hajimehoshi/oto/v2" "github.com/hajimehoshi/oto/v2"
) )
type SoundType int //SoundInfo contains static info about a loaded sound file
type SoundInfo struct {
Type SoundType
Mode SoundMode
const ( //Size is the sound's size in bytes
SoundType_Unknown SoundType = iota Size int64
SoundType_MP3 }
)
type SampleRate int type Sound struct {
Player oto.Player
const ( //FileDesc is the file descriptor of the sound file being streamed.
SampleRate_44100 SampleRate = 44100 //This is only set if sound is streamed, and is kept to ensure GC doesn't hit it
SampleRate_48000 SampleRate = 48000 FileDesc *os.File
)
type SoundChannelCount int //Data is an io.ReadSeeker over an open file or over a buffer containing the uncompressed sound file.
//Becomes nil after close
Data io.ReadSeeker
const ( Info SoundInfo
SoundChannelCount_1 SoundChannelCount = 1 }
SoundChannelCount_2 SoundChannelCount = 2
)
type SoundBitDepth int
const (
SoundBitDepth_1 SoundBitDepth = 1
SoundBitDepth_2 SoundBitDepth = 2
)
type SoundMode int
const (
SoundMode_Streaming SoundMode = iota
SoundMode_Memory
)
//Those values are set after Init
var ( var (
Ctx *oto.Context Ctx *oto.Context
SamplingRate SampleRate SamplingRate SampleRate
ChanCount SoundChannelCount ChanCount SoundChannelCount
BitDepth SoundBitDepth BitDepth SoundBitDepth
)
//Pre-defined errors //Pre-defined errors
ErrunknownSoundType = errors.New("unknown sound type. Sound file extensions must be: .mp3") var (
ErrunknownSoundType = errors.New("unknown sound type. Sound file extension must be one of: .mp3")
) )
//Init prepares the default audio device and does any required setup. //Init prepares the default audio device and does any required setup.
@ -77,29 +68,6 @@ func Init(sr SampleRate, chanCount SoundChannelCount, bitDepth SoundBitDepth) er
return nil return nil
} }
//SoundInfo contains static info about a loaded sound file
type SoundInfo struct {
Type SoundType
Mode SoundMode
//Size is the sound's size in bytes
Size int64
}
type Sound struct {
Player oto.Player
//FileDesc is the file descriptor of the sound file being streamed.
//This is only set if sound is streamed, and is kept to ensure GC doesn't hit it
FileDesc *os.File
//Bytes is an io.ReadSeeker over an open file or over a buffer containing the uncompressed sound file.
//Becomes nil after close
Bytes io.ReadSeeker
Info SoundInfo
}
//PlayAsync plays the sound in the background and returns //PlayAsync plays the sound in the background and returns
func (s *Sound) PlayAsync() { func (s *Sound) PlayAsync() {
s.Player.Play() s.Player.Play()
@ -135,7 +103,7 @@ func (s *Sound) RemainingTime() time.Duration {
} }
var currBytePos int64 var currBytePos int64
currBytePos, _ = s.Bytes.Seek(0, io.SeekCurrent) currBytePos, _ = s.Data.Seek(0, io.SeekCurrent)
currBytePos -= int64(s.Player.UnplayedBufferSize()) currBytePos -= int64(s.Player.UnplayedBufferSize())
lenInMS := float64(s.Info.Size-currBytePos) / float64(SamplingRate) / 4 * 1000 lenInMS := float64(s.Info.Size-currBytePos) / float64(SamplingRate) / 4 * 1000
@ -143,7 +111,7 @@ func (s *Sound) RemainingTime() time.Duration {
} }
func (s *Sound) IsClosed() bool { func (s *Sound) IsClosed() bool {
return s.Bytes == nil return s.Data == nil
} }
//Close will clean underlying resources, and the 'Ctx' and 'Bytes' fields will be made nil. //Close will clean underlying resources, and the 'Ctx' and 'Bytes' fields will be made nil.
@ -159,7 +127,7 @@ func (s *Sound) Close() error {
fdErr = s.FileDesc.Close() fdErr = s.FileDesc.Close()
} }
s.Bytes = nil s.Data = nil
playerErr := s.Player.Close() playerErr := s.Player.Close()
if playerErr == nil && fdErr == nil { if playerErr == nil && fdErr == nil {
@ -214,7 +182,7 @@ func NewSoundStreaming(fpath string) (s *Sound, err error) {
s.Info.Size = dec.Length() s.Info.Size = dec.Length()
s.Player = Ctx.NewPlayer(dec) s.Player = Ctx.NewPlayer(dec)
s.Bytes = dec s.Data = dec
} }
return s, nil return s, nil
@ -254,9 +222,15 @@ func NewSoundMem(fpath string) (s *Sound, err error) {
return nil, err return nil, err
} }
s.Bytes = dec finalBuf, err := ReadAllFromReader(dec, 0, uint64(dec.Length()))
s.Info.Size = dec.Length() if err != nil {
s.Player = Ctx.NewPlayer(dec) return nil, err
}
sb := &SoundBuffer{Data: finalBuf}
s.Data = sb
s.Player = Ctx.NewPlayer(sb)
s.Info.Size = int64(len(sb.Data))
} }
return s, nil return s, nil
@ -272,3 +246,36 @@ func GetSoundFileType(fpath string) SoundType {
return SoundType_Unknown return SoundType_Unknown
} }
} }
//ReadAllFromReader takes an io.Reader and reads until error or io.EOF.
//
//If io.EOF is reached then read bytes are returned with a nil error.
//If the reader returns an error that's not io.EOF then everything read till that point is returned along with the error
//
//readingBufSize is the buffer used to read from reader.Read(). Bigger values might read more efficiently.
//If readingBufSize<4096 then readingBufSize is set to 4096
//
//ouputBufSize is used to set the capacity of the final buffer to be returned. This can greatly improve performance
//if you know the size of the output. It is allowed to have an outputBufSize that's smaller or larger than what the reader
//ends up returning
func ReadAllFromReader(reader io.Reader, readingBufSize, ouputBufSize uint64) ([]byte, error) {
if readingBufSize < 4096 {
readingBufSize = 4096
}
tempBuf := make([]byte, readingBufSize)
finalBuf := make([]byte, 0, ouputBufSize)
for {
readBytesCount, err := reader.Read(tempBuf)
finalBuf = append(finalBuf, tempBuf[:readBytesCount]...)
if err != nil {
if err == io.EOF {
return finalBuf, nil
}
return finalBuf, err
}
}
}

View File

@ -84,7 +84,8 @@ func TestSound(t *testing.T) {
} }
s.PlaySync() s.PlaySync()
//Test repeat playing
s.Player.Reset() s.Player.Reset()
s.Bytes.Seek(0, io.SeekStart) s.Data.Seek(0, io.SeekStart)
s.PlaySync() s.PlaySync()
} }