Streaming and in-mem sound playing

This commit is contained in:
bloeys
2022-06-18 10:45:19 +04:00
parent 8b1456dd46
commit dd0c21efc7
2 changed files with 196 additions and 46 deletions

173
wavy.go
View File

@ -1 +1,174 @@
package wavy 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
}

View File

@ -3,13 +3,10 @@ package wavy_test
import ( import (
"io" "io"
"math" "math"
"os"
"runtime"
"testing" "testing"
"time" "time"
"github.com/hajimehoshi/go-mp3" "github.com/bloeys/wavy"
"github.com/hajimehoshi/oto/v2"
) )
const ( const (
@ -101,55 +98,35 @@ func NewSineWave(freq float64, duration time.Duration) *SineWave {
} }
} }
func TestWavy(t *testing.T) { func TestSound(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) {
audioFPath := "./test_audio_files/Fatiha.mp3" 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 { 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 return
} }
c, ready, err := oto.NewContext(dec.SampleRate(), 2, 2) s.PlayAsync()
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()
time.Sleep(1 * time.Second) time.Sleep(1 * time.Second)
if err := s.Close(); err != nil {
//This is to ensure GC doesn't collect player/context. Without it no sound might play, or plays for very small amount of time. t.Errorf("Closing streaming sound failed. Err: %s\n", err)
runtime.KeepAlive(player) return
}
//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
}
} }