From 2af3583f6f0559899d718770a844af8cd7494f1c Mon Sep 17 00:00:00 2001 From: bloeys Date: Mon, 27 Jun 2022 03:36:39 +0400 Subject: [PATCH] Ogg streaming + better tests --- ogg_streamer.go | 75 +++++++++++++++++++++++++++++++++++++++++++++++++ wav_streamer.go | 16 ++++++----- wavy.go | 21 ++++++++------ wavy_test.go | 48 +++++++++++++++++++++++-------- 4 files changed, 132 insertions(+), 28 deletions(-) create mode 100755 ogg_streamer.go diff --git a/ogg_streamer.go b/ogg_streamer.go new file mode 100755 index 0000000..2b37e8a --- /dev/null +++ b/ogg_streamer.go @@ -0,0 +1,75 @@ +package wavy + +import ( + "io" + "os" + "sync" + + "github.com/jfreymuth/oggvorbis" +) + +var _ io.ReadSeeker = &OggStreamer{} + +type OggStreamer struct { + F *os.File + Dec *oggvorbis.Reader + + //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 *OggStreamer) Read(outBuf []byte) (floatsRead int, err error) { + + ws.mutex.Lock() + + readerBuf := make([]float32, len(outBuf)/2) + floatsRead, err = ws.Dec.Read(readerBuf) + F32ToUnsignedPCM16(readerBuf[:floatsRead], outBuf) + + ws.mutex.Unlock() + + return floatsRead * 2, err +} + +func (ws *OggStreamer) Seek(offset int64, whence int) (int64, error) { + + ws.mutex.Lock() + defer ws.mutex.Unlock() + + //This is because ogg expects position in samples not bytes + offset /= BytesPerSample + + switch whence { + case io.SeekStart: + if err := ws.Dec.SetPosition(offset); err != nil { + return 0, err + } + + case io.SeekCurrent: + + if err := ws.Dec.SetPosition(ws.Dec.Position() + offset); err != nil { + return 0, err + } + + case io.SeekEnd: + if err := ws.Dec.SetPosition(ws.Dec.Length() + offset); err != nil { + return 0, err + } + } + + return ws.Dec.Position() * BytesPerSample, nil +} + +//Size returns number of bytes +func (ws *OggStreamer) Size() int64 { + return ws.Dec.Length() * BytesPerSample +} + +func NewOggStreamer(f *os.File, dec *oggvorbis.Reader) *OggStreamer { + return &OggStreamer{ + F: f, + Dec: dec, + mutex: sync.Mutex{}, + } +} diff --git a/wav_streamer.go b/wav_streamer.go index ab7c1f6..16288cd 100755 --- a/wav_streamer.go +++ b/wav_streamer.go @@ -36,13 +36,14 @@ func (ws *WavStreamer) Seek(offset int64, whence int) (int64, error) { ws.mutex.Lock() defer ws.mutex.Unlock() + //This will only seek the underlying file but not the actual decoder because it can't seek 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. + //Since underlying decoder can't seek back, if the requested movement is back we have to rewind the decoder + //then seek forward to the requested position. if n < ws.Pos { err = ws.Dec.Rewind() @@ -50,15 +51,14 @@ func (ws *WavStreamer) Seek(offset int64, whence int) (int64, error) { return 0, err } - //Anything before PCMStart is not valid sound, so the minimum seek back we allow is till PCMStart. - if n >= ws.PCMStart { - + //Anything before PCMStart is not valid sound, so the minimum seek back we allow is PCMStart + if n < ws.PCMStart { + n = ws.PCMStart + } else { n, err = ws.Dec.Seek(offset, whence) if err != nil { return n, err } - } else { - n = ws.PCMStart } } @@ -66,6 +66,7 @@ func (ws *WavStreamer) Seek(offset int64, whence int) (int64, error) { return n, err } +//Size returns number of bytes func (ws *WavStreamer) Size() int64 { return ws.Dec.PCMLen() } @@ -77,6 +78,7 @@ func NewWavStreamer(f *os.File, wavDec *wav.Decoder) (*WavStreamer, error) { return nil, err } + //The actual data starts somewhat within the file, not at 0 currPos, err := wavDec.Seek(0, 1) if err != nil { return nil, err diff --git a/wavy.go b/wavy.go index a3a57ca..c4b66d3 100644 --- a/wavy.go +++ b/wavy.go @@ -21,7 +21,6 @@ type SoundInfo struct { Type SoundType Mode SoundMode - //Size is the sound's size in bytes Size int64 } @@ -415,15 +414,16 @@ func soundFromFile(f *os.File, s *Sound) error { s.Info.Size = ws.Size() } else if s.Info.Type == SoundType_OGG { - soundData, _, err := oggvorbis.ReadAll(f) + oggReader, err := oggvorbis.NewReader(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)) + oggStreamer := NewOggStreamer(f, oggReader) + + s.Data = oggStreamer + s.Player = Ctx.NewPlayer(oggStreamer) + s.Info.Size = oggStreamer.Size() } if s.Data == nil { @@ -510,7 +510,7 @@ func decodeSoundFromReaderSeeker(r io.ReadSeeker, s *Sound) error { return err } - sb := &SoundBuffer{Data: F32ToUnsignedPCM16(soundData)} + sb := &SoundBuffer{Data: F32ToUnsignedPCM16(soundData, nil)} s.Data = sb s.Player = Ctx.NewPlayer(sb) s.Info.Size = int64(len(sb.Data)) @@ -599,9 +599,12 @@ func clamp01F64(x float64) float64 { //F32ToUnsignedPCM16 takes PCM data stored as float32 between [-1, 1] //and returns a byte array of uint16, where each two subsequent bytes represent one uint16. -func F32ToUnsignedPCM16(fs []float32) []byte { +func F32ToUnsignedPCM16(fs []float32, outBuf []byte) []byte { + + if outBuf == nil { + outBuf = make([]byte, len(fs)*2) + } - outBuf := make([]byte, len(fs)*2) for i := 0; i < len(fs); i++ { //Remap [-1,1]->[-32768, 32767], then re-interprets the int16 as a uint16. diff --git a/wavy_test.go b/wavy_test.go index 81f14b5..4ca3661 100755 --- a/wavy_test.go +++ b/wavy_test.go @@ -7,19 +7,29 @@ import ( "github.com/bloeys/wavy" ) -func TestSound(t *testing.T) { +func TestWavy(t *testing.T) { + t.Run("Init", TestInit) + t.Run("MP3", MP3Subtest) + t.Run("Wav", WavSubtest) + t.Run("Ogg", OggSubtest) +} + +func TestInit(t *testing.T) { err := wavy.Init(wavy.SampleRate_44100, wavy.SoundChannelCount_2, wavy.SoundBitDepth_2) if err != nil { t.Errorf("Failed to init wavy. Err: %s\n", err) return } +} + +func MP3Subtest(t *testing.T) { const fatihaFilepath = "./test_audio_files/Fatiha.mp3" const tadaFilepath = "./test_audio_files/tada.mp3" const fatihaLenMS = 55484 - //Streaming + //Mp3 streaming s, err := wavy.NewSoundStreaming(fatihaFilepath) if err != nil { t.Errorf("Failed to load streaming sound with path '%s'. Err: %s\n", fatihaFilepath, err) @@ -47,7 +57,7 @@ func TestSound(t *testing.T) { return } - //In-Memory + //Mp3 in-memory s, err = wavy.NewSoundMem(fatihaFilepath) if err != nil { t.Errorf("Failed to load memory sound with path '%s'. Err: %s\n", fatihaFilepath, err) @@ -75,7 +85,7 @@ func TestSound(t *testing.T) { return } - //Memory 'tada.mp3' + //'tada.mp3' memory s, err = wavy.NewSoundMem(tadaFilepath) if err != nil { t.Errorf("Failed to load memory sound with path '%s'. Err: %s\n", tadaFilepath, err) @@ -103,35 +113,49 @@ func TestSound(t *testing.T) { s3 := wavy.ClipInMemSoundPercent(s2, 0, 0.25) s3.LoopAsync(3) s3.WaitLoop() +} + +func WavSubtest(t *testing.T) { - //Wav const wavFPath = "./test_audio_files/camera.wav" - s, err = wavy.NewSoundMem(wavFPath) + + s, err := wavy.NewSoundMem(wavFPath) if err != nil { t.Errorf("Failed to load memory sound with path '%s'. Err: %s\n", wavFPath, err) return } s.PlaySync() - //Streaming wav + //Wav streaming s, err = wavy.NewSoundStreaming(wavFPath) if err != nil { - t.Errorf("Failed to load streaming sound with path '%s'. Err: %s\n", tadaFilepath, err) + t.Errorf("Failed to load streaming sound with path '%s'. Err: %s\n", wavFPath, err) return } - s.SeekToPercent(0.0) s.PlaySync() - s.SeekToPercent(0.0) + s.SeekToPercent(0.5) s.PlaySync() +} + +func OggSubtest(t *testing.T) { - //Ogg const oggFPath = "./test_audio_files/camera.ogg" - s, err = wavy.NewSoundMem(oggFPath) + s, err := wavy.NewSoundMem(oggFPath) if err != nil { t.Errorf("Failed to load memory sound with path '%s'. Err: %s\n", oggFPath, err) return } s.PlaySync() + + //Ogg streaming + s, err = wavy.NewSoundStreaming(oggFPath) + if err != nil { + t.Errorf("Failed to load streaming sound with path '%s'. Err: %s\n", oggFPath, err) + return + } + s.PlaySync() + s.SeekToPercent(.5) + s.PlaySync() } func TestByteCountFromPlayTime(t *testing.T) {