diff --git a/sound_buffer.go b/sound_buffer.go new file mode 100755 index 0000000..7b97b6e --- /dev/null +++ b/sound_buffer.go @@ -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, + } +} diff --git a/sound_enums.go b/sound_enums.go new file mode 100755 index 0000000..b21251d --- /dev/null +++ b/sound_enums.go @@ -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 +) diff --git a/wavy.go b/wavy.go index ea01a9b..ce38bd1 100644 --- a/wavy.go +++ b/wavy.go @@ -14,49 +14,40 @@ import ( "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 ( - SoundType_Unknown SoundType = iota - SoundType_MP3 -) + //Size is the sound's size in bytes + Size int64 +} -type SampleRate int +type Sound struct { + Player oto.Player -const ( - SampleRate_44100 SampleRate = 44100 - SampleRate_48000 SampleRate = 48000 -) + //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 -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 ( - 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 -) + Info SoundInfo +} +//Those values are set after Init var ( Ctx *oto.Context SamplingRate SampleRate ChanCount SoundChannelCount BitDepth SoundBitDepth +) - //Pre-defined errors - ErrunknownSoundType = errors.New("unknown sound type. Sound file extensions must be: .mp3") +//Pre-defined errors +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. @@ -77,29 +68,6 @@ func Init(sr SampleRate, chanCount SoundChannelCount, bitDepth SoundBitDepth) er 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 func (s *Sound) PlayAsync() { s.Player.Play() @@ -135,7 +103,7 @@ func (s *Sound) RemainingTime() time.Duration { } var currBytePos int64 - currBytePos, _ = s.Bytes.Seek(0, io.SeekCurrent) + currBytePos, _ = s.Data.Seek(0, io.SeekCurrent) currBytePos -= int64(s.Player.UnplayedBufferSize()) 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 { - return s.Bytes == nil + return s.Data == 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() } - s.Bytes = nil + s.Data = nil playerErr := s.Player.Close() if playerErr == nil && fdErr == nil { @@ -214,7 +182,7 @@ func NewSoundStreaming(fpath string) (s *Sound, err error) { s.Info.Size = dec.Length() s.Player = Ctx.NewPlayer(dec) - s.Bytes = dec + s.Data = dec } return s, nil @@ -254,9 +222,15 @@ func NewSoundMem(fpath string) (s *Sound, err error) { return nil, err } - s.Bytes = dec - s.Info.Size = dec.Length() - s.Player = Ctx.NewPlayer(dec) + finalBuf, err := ReadAllFromReader(dec, 0, uint64(dec.Length())) + if err != nil { + 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 @@ -272,3 +246,36 @@ func GetSoundFileType(fpath string) SoundType { 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 + } + } +} diff --git a/wavy_test.go b/wavy_test.go index 8ce0b72..9c4524c 100755 --- a/wavy_test.go +++ b/wavy_test.go @@ -84,7 +84,8 @@ func TestSound(t *testing.T) { } s.PlaySync() + //Test repeat playing s.Player.Reset() - s.Bytes.Seek(0, io.SeekStart) + s.Data.Seek(0, io.SeekStart) s.PlaySync() }