mirror of
https://github.com/bloeys/wavy.git
synced 2025-12-29 09:28:19 +00:00
Properly stream wav files+reduce effect of oto bug
This commit is contained in:
BIN
test_audio_files/camera.mp3
Executable file
BIN
test_audio_files/camera.mp3
Executable file
Binary file not shown.
92
wav_streamer.go
Executable file
92
wav_streamer.go
Executable file
@ -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
|
||||||
|
}
|
||||||
66
wavy.go
66
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
|
//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) {
|
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() {
|
if !s.IsPlaying() {
|
||||||
s.Player.Reset()
|
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.
|
//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]
|
//t is clamped between [0, totalTime]
|
||||||
func (s *Sound) SeekToTime(t time.Duration) {
|
func (s *Sound) SeekToTime(t time.Duration) {
|
||||||
|
|
||||||
if !s.IsPlaying() {
|
|
||||||
s.Player.Reset()
|
|
||||||
}
|
|
||||||
|
|
||||||
byteCount := ByteCountFromPlayTime(t)
|
byteCount := ByteCountFromPlayTime(t)
|
||||||
if byteCount < 0 {
|
if byteCount < 0 {
|
||||||
byteCount = 0
|
byteCount = 0
|
||||||
@ -262,6 +260,10 @@ func (s *Sound) SeekToTime(t time.Duration) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
s.Data.Seek(byteCount, io.SeekStart)
|
s.Data.Seek(byteCount, io.SeekStart)
|
||||||
|
|
||||||
|
if !s.IsPlaying() {
|
||||||
|
s.Player.Reset()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Sound) IsClosed() bool {
|
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 {
|
if err != nil {
|
||||||
return nil, getLoadingErr(fpath, err)
|
return nil, getLoadingErr(fpath, err)
|
||||||
}
|
}
|
||||||
@ -389,6 +391,48 @@ func NewSoundStreaming(fpath string) (s *Sound, err error) {
|
|||||||
return s, nil
|
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
|
//NewSoundMem loads the entire sound file into memory
|
||||||
func NewSoundMem(fpath string) (s *Sound, err error) {
|
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 {
|
if err != nil {
|
||||||
return nil, getLoadingErr(fpath, err)
|
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())
|
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 {
|
if s.Info.Type == SoundType_MP3 {
|
||||||
|
|
||||||
|
|||||||
11
wavy_test.go
11
wavy_test.go
@ -113,6 +113,17 @@ func TestSound(t *testing.T) {
|
|||||||
}
|
}
|
||||||
s.PlaySync()
|
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
|
//Ogg
|
||||||
const oggFPath = "./test_audio_files/camera.ogg"
|
const oggFPath = "./test_audio_files/camera.ogg"
|
||||||
s, err = wavy.NewSoundMem(oggFPath)
|
s, err = wavy.NewSoundMem(oggFPath)
|
||||||
|
|||||||
Reference in New Issue
Block a user