From dd0c21efc78db1f9a9bab053bfe39618bbf79833 Mon Sep 17 00:00:00 2001 From: bloeys Date: Sat, 18 Jun 2022 10:45:19 +0400 Subject: [PATCH] Streaming and in-mem sound playing --- wavy.go | 173 +++++++++++++++++++++++++++++++++++++++++++++++++++ wavy_test.go | 69 +++++++------------- 2 files changed, 196 insertions(+), 46 deletions(-) diff --git a/wavy.go b/wavy.go index ea378f4..4afdc39 100644 --- a/wavy.go +++ b/wavy.go @@ -1 +1,174 @@ package wavy + +import ( + "bytes" + "errors" + "fmt" + "os" + "strings" + "time" + + "github.com/hajimehoshi/go-mp3" + "github.com/hajimehoshi/oto/v2" +) + +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 +) + +var ( + ErrunknownSoundType = errors.New("unknown sound type. Sound file extensions must be: .mp3") +) + +type Sound struct { + 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 *os.File + + //BytesReader is a reader from a buffer containing the entire sound file + BytesReader *bytes.Reader +} + +func (s *Sound) PlayAsync() { + s.Player.Play() +} + +func (s *Sound) PlaySync() { + + s.Player.Play() + for s.Player.IsPlaying() { + time.Sleep(100 * time.Millisecond) + } +} + +func (s *Sound) Close() error { + + var fdErr error = nil + if s.FileDesc != nil { + fdErr = s.FileDesc.Close() + } + + playerErr := s.Player.Close() + if playerErr == nil && fdErr == nil { + return nil + } + + if playerErr != nil && fdErr != nil { + return fmt.Errorf("closingFileErr: %s; underlyingPlayerErr: %s", fdErr.Error(), playerErr.Error()) + } + + if playerErr != nil { + return playerErr + } + + return fdErr +} + +//NewSoundStreaming plays sound by streaming from a file, so no need to load the entire file into memory. +func NewSoundStreaming(fpath string, sr SampleRate, chanCount SoundChannelCount, bitDepth SoundBitDepth) (s *Sound, err error) { + + //Error checking filetype + soundType := SoundType_Unknown + if strings.HasSuffix(fpath, ".mp3") { + soundType = SoundType_MP3 + } + + if soundType == SoundType_Unknown { + return nil, ErrunknownSoundType + } + + //Preparing oto context + otoCtx, readyChan, err := oto.NewContext(int(sr), int(chanCount), int(bitDepth)) + if err != nil { + return nil, err + } + <-readyChan + + //We read file but don't close so the player can stream the file any time later + file, err := os.Open(fpath) + if err != nil { + return nil, err + } + + //Load file depending on type + s = &Sound{Ctx: otoCtx, Type: soundType, FileDesc: file} + if soundType == SoundType_MP3 { + + dec, err := mp3.NewDecoder(file) + if err != nil { + return nil, err + } + + s.Player = otoCtx.NewPlayer(dec) + } + + return s, nil +} + +//NewSoundMem loads the entire sound file into memory and plays from that +func NewSoundMem(fpath string, sr SampleRate, chanCount SoundChannelCount, bitDepth SoundBitDepth) (s *Sound, err error) { + + //Error checking filetype + soundType := SoundType_Unknown + if strings.HasSuffix(fpath, ".mp3") { + soundType = SoundType_MP3 + } + + if soundType == SoundType_Unknown { + return nil, ErrunknownSoundType + } + + //Preparing oto context + otoCtx, readyChan, err := oto.NewContext(int(sr), int(chanCount), int(bitDepth)) + if err != nil { + return nil, err + } + <-readyChan + + fileBytes, err := os.ReadFile(fpath) + if err != nil { + return nil, err + } + bytesReader := bytes.NewReader(fileBytes) + + //Load file depending on type + s = &Sound{Ctx: otoCtx, Type: soundType, BytesReader: bytesReader} + if soundType == SoundType_MP3 { + + dec, err := mp3.NewDecoder(bytesReader) + if err != nil { + return nil, err + } + + s.Player = otoCtx.NewPlayer(dec) + } + + return s, nil +} diff --git a/wavy_test.go b/wavy_test.go index 996c017..16a0246 100755 --- a/wavy_test.go +++ b/wavy_test.go @@ -3,13 +3,10 @@ package wavy_test import ( "io" "math" - "os" - "runtime" "testing" "time" - "github.com/hajimehoshi/go-mp3" - "github.com/hajimehoshi/oto/v2" + "github.com/bloeys/wavy" ) const ( @@ -101,55 +98,35 @@ func NewSineWave(freq float64, duration time.Duration) *SineWave { } } -func TestWavy(t *testing.T) { - - const freqToUse = 523.3 - - c, ready, err := oto.NewContext(44100, 2, 2) - if err != nil { - t.Errorf("Failed to create oto context. Err: %e\n", err) - return - } - <-ready - - playDuration := 1 * time.Second - player := c.NewPlayer(NewSineWave(freqToUse, playDuration)) - player.SetVolume(0.75) - player.Play() - - time.Sleep(playDuration) - runtime.KeepAlive(player) -} - -func TestMP3(t *testing.T) { +func TestSound(t *testing.T) { audioFPath := "./test_audio_files/Fatiha.mp3" - f, err := os.Open(audioFPath) - if err != nil { - t.Errorf("Failed to open '%s'. Err: %s\n", audioFPath, err) - return - } - defer f.Close() - dec, err := mp3.NewDecoder(f) + //Streaming + s, err := wavy.NewSoundStreaming(audioFPath, wavy.SampleRate_44100, wavy.SoundChannelCount_2, wavy.SoundBitDepth_2) if err != nil { - t.Errorf("Failed to decode mp3 file. Err: %s\n", err) + t.Errorf("Failed to load new sound with path '%s'. Err: %s\n", audioFPath, err) return } - c, ready, err := oto.NewContext(dec.SampleRate(), 2, 2) - if err != nil { - t.Errorf("Failed to create oto context. Err: %s\n", err) - return - } - <-ready - - player := c.NewPlayer(dec) - player.SetVolume(0.75) - player.Play() - + s.PlayAsync() time.Sleep(1 * time.Second) + if err := s.Close(); err != nil { + t.Errorf("Closing streaming sound failed. Err: %s\n", err) + return + } - //This is to ensure GC doesn't collect player/context. Without it no sound might play, or plays for very small amount of time. - runtime.KeepAlive(player) + //In-Memory + s, err = wavy.NewSoundMem(audioFPath, 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) + return + } + + s.PlayAsync() + time.Sleep(1 * time.Second) + if err := s.Close(); err != nil { + t.Errorf("Closing in-memory sound failed. Err: %s\n", err) + return + } }