diff --git a/test_audio_files/camera.mp3 b/test_audio_files/camera.mp3 new file mode 100755 index 0000000..01b8140 Binary files /dev/null and b/test_audio_files/camera.mp3 differ diff --git a/wav_streamer.go b/wav_streamer.go new file mode 100755 index 0000000..ab7c1f6 --- /dev/null +++ b/wav_streamer.go @@ -0,0 +1,92 @@ +package wavy + +import ( + "io" + "os" + "sync" + + "github.com/go-audio/wav" +) + +var _ io.ReadSeeker = &WavStreamer{} + +type WavStreamer struct { + F *os.File + Dec *wav.Decoder + Pos int64 + PCMStart int64 + + //TODO: This is currently needed because of https://github.com/hajimehoshi/oto/issues/171 + //We should be able to delete once its resolved + mutex sync.Mutex +} + +func (ws *WavStreamer) Read(outBuf []byte) (bytesRead int, err error) { + + ws.mutex.Lock() + bytesRead, err = ws.Dec.PCMChunk.Read(outBuf) + ws.Pos += int64(bytesRead) + ws.mutex.Unlock() + + return bytesRead, err +} + +func (ws *WavStreamer) Seek(offset int64, whence int) (int64, error) { + + ws.mutex.Lock() + defer ws.mutex.Unlock() + + n, err := ws.Dec.Seek(offset, whence) + if err != nil { + return n, err + } + + //The underlying parser can not seek back, so in that case we have to rewind the decoder + //then seek forward. + if n < ws.Pos { + + err = ws.Dec.Rewind() + if err != nil { + return 0, err + } + + //Anything before PCMStart is not valid sound, so the minimum seek back we allow is till PCMStart. + if n >= ws.PCMStart { + + n, err = ws.Dec.Seek(offset, whence) + if err != nil { + return n, err + } + } else { + n = ws.PCMStart + } + } + + ws.Pos = n + return n, err +} + +func (ws *WavStreamer) Size() int64 { + return ws.Dec.PCMLen() +} + +func NewWavStreamer(f *os.File, wavDec *wav.Decoder) (*WavStreamer, error) { + + err := wavDec.FwdToPCM() + if err != nil { + return nil, err + } + + currPos, err := wavDec.Seek(0, 1) + if err != nil { + return nil, err + } + + return &WavStreamer{ + F: f, + Dec: wavDec, + Pos: currPos, + PCMStart: currPos, + mutex: sync.Mutex{}, + }, nil +} diff --git a/wavy.go b/wavy.go index 669ffd1..a3a57ca 100644 --- a/wavy.go +++ b/wavy.go @@ -234,12 +234,14 @@ func (s *Sound) IsPlaying() bool { //percent is clamped [0,1], so passing <0 is the same as zero, and >1 is the same as 1 func (s *Sound) SeekToPercent(percent float64) { + percent = clamp01F64(percent) + s.Data.Seek(int64(float64(s.Info.Size)*percent), io.SeekStart) + + //NOTE: Due to https://github.com/hajimehoshi/oto/issues/171, it is safer to seek before reset so we don't seek while a read is happening. + //This can still happen though if for example sound was paused midway then seeked, as read would be getting called if !s.IsPlaying() { s.Player.Reset() } - - percent = clamp01F64(percent) - s.Data.Seek(int64(float64(s.Info.Size)*percent), io.SeekStart) } //SeekToTime moves the current position of the sound to the given duration. @@ -250,10 +252,6 @@ func (s *Sound) SeekToPercent(percent float64) { //t is clamped between [0, totalTime] func (s *Sound) SeekToTime(t time.Duration) { - if !s.IsPlaying() { - s.Player.Reset() - } - byteCount := ByteCountFromPlayTime(t) if byteCount < 0 { byteCount = 0 @@ -262,6 +260,10 @@ func (s *Sound) SeekToTime(t time.Duration) { } s.Data.Seek(byteCount, io.SeekStart) + + if !s.IsPlaying() { + s.Player.Reset() + } } func (s *Sound) IsClosed() bool { @@ -381,7 +383,7 @@ func NewSoundStreaming(fpath string) (s *Sound, err error) { }, } - err = soundFromReaderSeeker(file, s) + err = soundFromFile(file, s) if err != nil { return nil, getLoadingErr(fpath, err) } @@ -389,6 +391,48 @@ func NewSoundStreaming(fpath string) (s *Sound, err error) { return s, nil } +func soundFromFile(f *os.File, s *Sound) error { + + if s.Info.Type == SoundType_MP3 { + + dec, err := mp3.NewDecoder(f) + if err != nil { + return err + } + + s.Data = dec + s.Player = Ctx.NewPlayer(dec) + s.Info.Size = dec.Length() + } else if s.Info.Type == SoundType_WAV { + + ws, err := NewWavStreamer(f, wav.NewDecoder(f)) + if err != nil { + return err + } + + s.Data = ws + s.Player = Ctx.NewPlayer(ws) + s.Info.Size = ws.Size() + } else if s.Info.Type == SoundType_OGG { + + soundData, _, err := oggvorbis.ReadAll(f) + if err != nil { + return err + } + + sb := &SoundBuffer{Data: F32ToUnsignedPCM16(soundData)} + s.Data = sb + s.Player = Ctx.NewPlayer(sb) + s.Info.Size = int64(len(sb.Data)) + } + + if s.Data == nil { + panic("invalid sound type. This is probably a bug!") + } + + return nil +} + //NewSoundMem loads the entire sound file into memory func NewSoundMem(fpath string) (s *Sound, err error) { @@ -410,7 +454,7 @@ func NewSoundMem(fpath string) (s *Sound, err error) { }, } - err = soundFromReaderSeeker(bytesReader, s) + err = decodeSoundFromReaderSeeker(bytesReader, s) if err != nil { return nil, getLoadingErr(fpath, err) } @@ -422,7 +466,9 @@ func getLoadingErr(fpath string, err error) error { return fmt.Errorf("failed to load '%s' with err '%s'", fpath, err.Error()) } -func soundFromReaderSeeker(r io.ReadSeeker, s *Sound) error { +//decodeSoundFromReaderSeeker reads and decodes till EOF, and places the final +//PCM16 data in a buffer, thus producing an in-memory sound +func decodeSoundFromReaderSeeker(r io.ReadSeeker, s *Sound) error { if s.Info.Type == SoundType_MP3 { diff --git a/wavy_test.go b/wavy_test.go index 8519528..81f14b5 100755 --- a/wavy_test.go +++ b/wavy_test.go @@ -113,6 +113,17 @@ func TestSound(t *testing.T) { } s.PlaySync() + //Streaming wav + s, err = wavy.NewSoundStreaming(wavFPath) + if err != nil { + t.Errorf("Failed to load streaming sound with path '%s'. Err: %s\n", tadaFilepath, err) + return + } + s.SeekToPercent(0.0) + s.PlaySync() + s.SeekToPercent(0.0) + s.PlaySync() + //Ogg const oggFPath = "./test_audio_files/camera.ogg" s, err = wavy.NewSoundMem(oggFPath)