mirror of
https://github.com/bloeys/wavy.git
synced 2025-12-29 09:28:19 +00:00
Use SoundBuffer for memory sounds
This commit is contained in:
70
sound_buffer.go
Executable file
70
sound_buffer.go
Executable 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
36
sound_enums.go
Executable 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
129
wavy.go
@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@ -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()
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user