From a0af1959e02f6923242b5e26bcc5372eb7e968b7 Mon Sep 17 00:00:00 2001 From: bloeys Date: Sat, 18 Jun 2022 13:26:28 +0400 Subject: [PATCH] Sound info+sound time+remaining time --- wavy.go | 108 +++++++++++++++++++++++++++++++++++++++++++++++---- wavy_test.go | 37 +++++++++++++++--- 2 files changed, 133 insertions(+), 12 deletions(-) diff --git a/wavy.go b/wavy.go index 4afdc39..9f6e4d4 100644 --- a/wavy.go +++ b/wavy.go @@ -4,6 +4,7 @@ import ( "bytes" "errors" "fmt" + "io" "os" "strings" "time" @@ -40,20 +41,44 @@ const ( SoundBitDepth_2 SoundBitDepth = 2 ) +type SoundMode int + +const ( + SoundMode_Streaming SoundMode = iota + SoundMode_Memory +) + var ( ErrunknownSoundType = errors.New("unknown sound type. Sound file extensions must be: .mp3") ) +//SoundInfo contains static info about a loaded sound file +type SoundInfo struct { + Type SoundType + Mode SoundMode + + SamplingRate SampleRate + ChanCount SoundChannelCount + BitDepth SoundBitDepth + + //Size is the sound's size in bytes + Size int64 +} + type Sound struct { + //Becomes nil after close Ctx *oto.Context Player oto.Player - Type SoundType - //FileDesc is the file descriptor of the sound file being streamed. This is only set if NewSoundStreaming is used + //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 - //BytesReader is a reader from a buffer containing the entire sound file - BytesReader *bytes.Reader + //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 } func (s *Sound) PlayAsync() { @@ -63,19 +88,61 @@ func (s *Sound) PlayAsync() { func (s *Sound) PlaySync() { s.Player.Play() + time.Sleep(s.TotalTime()) + + //Should never run, but just in case TotalTimeMS was a bit inaccurate for s.Player.IsPlaying() { - time.Sleep(100 * time.Millisecond) } } +//TotalTime returns the time taken to play the entire sound. +//Safe to use after close +func (s *Sound) TotalTime() time.Duration { + //Number of bytes divided by sampling rate (which is bytes consumed per second), then divide by 4 because each sample is 4 bytes in go-mp3 + lenInMS := float64(s.Info.Size) / float64(s.Info.SamplingRate) / 4 * 1000 + return time.Duration(lenInMS) * time.Millisecond +} + +//RemainingTime returns the time left in the clip, which is affected by pausing/resetting/seeking of the sound. +//Returns zero after close +func (s *Sound) RemainingTime() time.Duration { + + if s.IsClosed() { + return 0 + } + + var currBytePos int64 + if s.Info.Mode == SoundMode_Streaming { + currBytePos, _ = s.Bytes.Seek(0, io.SeekCurrent) + } else { + currBytePos, _ = s.Bytes.Seek(0, io.SeekCurrent) + } + + lenInMS := float64(s.Info.Size-currBytePos) / float64(s.Info.SamplingRate) / 4 * 1000 + return time.Duration(lenInMS) * time.Millisecond +} + +func (s *Sound) IsClosed() bool { + return s.Ctx == nil +} + +//Close will clean underlying resources, and the 'Ctx' and 'Bytes' fields will be made nil. +//Repeated calls are no-ops func (s *Sound) Close() error { + if s.IsClosed() { + return nil + } + var fdErr error = nil if s.FileDesc != nil { fdErr = s.FileDesc.Close() } + s.Ctx = nil + s.Bytes = nil playerErr := s.Player.Close() + if playerErr == nil && fdErr == nil { return nil } @@ -117,8 +184,20 @@ func NewSoundStreaming(fpath string, sr SampleRate, chanCount SoundChannelCount, return nil, err } + s = &Sound{ + Ctx: otoCtx, + FileDesc: file, + Info: SoundInfo{ + Type: soundType, + Mode: SoundMode_Streaming, + + SamplingRate: sr, + ChanCount: chanCount, + BitDepth: bitDepth, + }, + } + //Load file depending on type - s = &Sound{Ctx: otoCtx, Type: soundType, FileDesc: file} if soundType == SoundType_MP3 { dec, err := mp3.NewDecoder(file) @@ -126,7 +205,9 @@ func NewSoundStreaming(fpath string, sr SampleRate, chanCount SoundChannelCount, return nil, err } + s.Info.Size = dec.Length() s.Player = otoCtx.NewPlayer(dec) + s.Bytes = dec } return s, nil @@ -156,10 +237,21 @@ func NewSoundMem(fpath string, sr SampleRate, chanCount SoundChannelCount, bitDe if err != nil { return nil, err } + bytesReader := bytes.NewReader(fileBytes) + s = &Sound{ + Ctx: otoCtx, + Info: SoundInfo{ + Type: soundType, + Mode: SoundMode_Memory, + + SamplingRate: sr, + ChanCount: chanCount, + BitDepth: bitDepth, + }, + } //Load file depending on type - s = &Sound{Ctx: otoCtx, Type: soundType, BytesReader: bytesReader} if soundType == SoundType_MP3 { dec, err := mp3.NewDecoder(bytesReader) @@ -167,6 +259,8 @@ func NewSoundMem(fpath string, sr SampleRate, chanCount SoundChannelCount, bitDe return nil, err } + s.Bytes = dec + s.Info.Size = dec.Length() s.Player = otoCtx.NewPlayer(dec) } diff --git a/wavy_test.go b/wavy_test.go index 16a0246..1e2172a 100755 --- a/wavy_test.go +++ b/wavy_test.go @@ -100,33 +100,60 @@ func NewSineWave(freq float64, duration time.Duration) *SineWave { func TestSound(t *testing.T) { - audioFPath := "./test_audio_files/Fatiha.mp3" + fatihaFilepath := "./test_audio_files/Fatiha.mp3" + const fatihaLenMS = 55484 //Streaming - s, err := wavy.NewSoundStreaming(audioFPath, wavy.SampleRate_44100, wavy.SoundChannelCount_2, wavy.SoundBitDepth_2) + s, err := wavy.NewSoundStreaming(fatihaFilepath, wavy.SampleRate_44100, wavy.SoundChannelCount_2, wavy.SoundBitDepth_2) if err != nil { - t.Errorf("Failed to load new sound with path '%s'. Err: %s\n", audioFPath, err) + t.Errorf("Failed to load streaming sound with path '%s'. Err: %s\n", fatihaFilepath, err) return } s.PlayAsync() time.Sleep(1 * time.Second) + + remTime := s.RemainingTime() + if remTime.Milliseconds() >= fatihaLenMS-900 { + t.Errorf("Expected time to be < %dms but got %dms in streaming sound\n", fatihaLenMS-900, remTime.Milliseconds()) + return + } + if err := s.Close(); err != nil { t.Errorf("Closing streaming sound failed. Err: %s\n", err) return } + totalTime := s.TotalTime() + if totalTime.Milliseconds() != fatihaLenMS { + t.Errorf("Expected time to be %dms but got %dms in streaming sound\n", fatihaLenMS, totalTime.Milliseconds()) + return + } + //In-Memory - s, err = wavy.NewSoundMem(audioFPath, wavy.SampleRate_44100, wavy.SoundChannelCount_2, wavy.SoundBitDepth_2) + s, err = wavy.NewSoundMem(fatihaFilepath, wavy.SampleRate_44100, wavy.SoundChannelCount_2, wavy.SoundBitDepth_2) if err != nil { - t.Errorf("Failed to load new sound with path '%s'. Err: %s\n", audioFPath, err) + t.Errorf("Failed to load memory sound with path '%s'. Err: %s\n", fatihaFilepath, err) return } s.PlayAsync() time.Sleep(1 * time.Second) + + remTime = s.RemainingTime() + if remTime.Milliseconds() >= fatihaLenMS-900 { + t.Errorf("Expected time to be < %dms but got %dms in memory sound\n", fatihaLenMS-900, remTime.Milliseconds()) + return + } + if err := s.Close(); err != nil { t.Errorf("Closing in-memory sound failed. Err: %s\n", err) return } + + totalTime = s.TotalTime() + if totalTime.Milliseconds() != fatihaLenMS { + t.Errorf("Expected time to be %dms but got %dms in memory sound\n", fatihaLenMS, totalTime.Milliseconds()) + return + } }