mirror of
https://github.com/bloeys/wavy.git
synced 2025-12-29 09:28:19 +00:00
Ensure only one context is used+consider unplayed buffer when playing sync
This commit is contained in:
86
wavy.go
86
wavy.go
@ -6,6 +6,7 @@ import (
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"path"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
@ -49,25 +50,43 @@ const (
|
||||
)
|
||||
|
||||
var (
|
||||
Ctx *oto.Context
|
||||
SamplingRate SampleRate
|
||||
ChanCount SoundChannelCount
|
||||
BitDepth SoundBitDepth
|
||||
|
||||
//Pre-defined errors
|
||||
ErrunknownSoundType = errors.New("unknown sound type. Sound file extensions must be: .mp3")
|
||||
)
|
||||
|
||||
//Init prepares the default audio device and does any required setup.
|
||||
//It must be called before loading any sounds
|
||||
func Init(sr SampleRate, chanCount SoundChannelCount, bitDepth SoundBitDepth) error {
|
||||
|
||||
otoCtx, readyChan, err := oto.NewContext(int(sr), int(chanCount), int(bitDepth))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
<-readyChan
|
||||
|
||||
Ctx = otoCtx
|
||||
SamplingRate = sr
|
||||
ChanCount = chanCount
|
||||
BitDepth = bitDepth
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
//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
|
||||
|
||||
//FileDesc is the file descriptor of the sound file being streamed.
|
||||
@ -94,8 +113,8 @@ func (s *Sound) PlaySync() {
|
||||
}
|
||||
|
||||
time.Sleep(s.RemainingTime())
|
||||
//Should never run, but just in case TotalTimeMS was a bit inaccurate
|
||||
for s.Player.IsPlaying() {
|
||||
for s.Player.IsPlaying() || s.Player.UnplayedBufferSize() > 0 {
|
||||
time.Sleep(time.Millisecond)
|
||||
}
|
||||
}
|
||||
|
||||
@ -103,7 +122,7 @@ func (s *Sound) PlaySync() {
|
||||
//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
|
||||
lenInMS := float64(s.Info.Size) / float64(SamplingRate) / 4 * 1000
|
||||
return time.Duration(lenInMS) * time.Millisecond
|
||||
}
|
||||
|
||||
@ -117,13 +136,14 @@ func (s *Sound) RemainingTime() time.Duration {
|
||||
|
||||
var currBytePos int64
|
||||
currBytePos, _ = s.Bytes.Seek(0, io.SeekCurrent)
|
||||
currBytePos -= int64(s.Player.UnplayedBufferSize())
|
||||
|
||||
lenInMS := float64(s.Info.Size-currBytePos) / float64(s.Info.SamplingRate) / 4 * 1000
|
||||
lenInMS := float64(s.Info.Size-currBytePos) / float64(SamplingRate) / 4 * 1000
|
||||
return time.Duration(lenInMS) * time.Millisecond
|
||||
}
|
||||
|
||||
func (s *Sound) IsClosed() bool {
|
||||
return s.Ctx == nil
|
||||
return s.Bytes == nil
|
||||
}
|
||||
|
||||
//Close will clean underlying resources, and the 'Ctx' and 'Bytes' fields will be made nil.
|
||||
@ -139,7 +159,6 @@ func (s *Sound) Close() error {
|
||||
fdErr = s.FileDesc.Close()
|
||||
}
|
||||
|
||||
s.Ctx = nil
|
||||
s.Bytes = nil
|
||||
playerErr := s.Player.Close()
|
||||
|
||||
@ -159,7 +178,7 @@ func (s *Sound) Close() error {
|
||||
}
|
||||
|
||||
//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) {
|
||||
func NewSoundStreaming(fpath string) (s *Sound, err error) {
|
||||
|
||||
//Error checking filetype
|
||||
soundType := SoundType_Unknown
|
||||
@ -171,13 +190,6 @@ func NewSoundStreaming(fpath string, sr SampleRate, chanCount SoundChannelCount,
|
||||
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 {
|
||||
@ -185,15 +197,10 @@ func NewSoundStreaming(fpath string, sr SampleRate, chanCount SoundChannelCount,
|
||||
}
|
||||
|
||||
s = &Sound{
|
||||
Ctx: otoCtx,
|
||||
FileDesc: file,
|
||||
Info: SoundInfo{
|
||||
Type: soundType,
|
||||
Mode: SoundMode_Streaming,
|
||||
|
||||
SamplingRate: sr,
|
||||
ChanCount: chanCount,
|
||||
BitDepth: bitDepth,
|
||||
},
|
||||
}
|
||||
|
||||
@ -206,7 +213,7 @@ func NewSoundStreaming(fpath string, sr SampleRate, chanCount SoundChannelCount,
|
||||
}
|
||||
|
||||
s.Info.Size = dec.Length()
|
||||
s.Player = otoCtx.NewPlayer(dec)
|
||||
s.Player = Ctx.NewPlayer(dec)
|
||||
s.Bytes = dec
|
||||
}
|
||||
|
||||
@ -214,7 +221,7 @@ func NewSoundStreaming(fpath string, sr SampleRate, chanCount SoundChannelCount,
|
||||
}
|
||||
|
||||
//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) {
|
||||
func NewSoundMem(fpath string) (s *Sound, err error) {
|
||||
|
||||
//Error checking filetype
|
||||
soundType := SoundType_Unknown
|
||||
@ -226,13 +233,6 @@ func NewSoundMem(fpath string, sr SampleRate, chanCount SoundChannelCount, bitDe
|
||||
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
|
||||
@ -240,14 +240,9 @@ func NewSoundMem(fpath string, sr SampleRate, chanCount SoundChannelCount, bitDe
|
||||
|
||||
bytesReader := bytes.NewReader(fileBytes)
|
||||
s = &Sound{
|
||||
Ctx: otoCtx,
|
||||
Info: SoundInfo{
|
||||
Type: soundType,
|
||||
Mode: SoundMode_Memory,
|
||||
|
||||
SamplingRate: sr,
|
||||
ChanCount: chanCount,
|
||||
BitDepth: bitDepth,
|
||||
},
|
||||
}
|
||||
|
||||
@ -261,8 +256,19 @@ func NewSoundMem(fpath string, sr SampleRate, chanCount SoundChannelCount, bitDe
|
||||
|
||||
s.Bytes = dec
|
||||
s.Info.Size = dec.Length()
|
||||
s.Player = otoCtx.NewPlayer(dec)
|
||||
s.Player = Ctx.NewPlayer(dec)
|
||||
}
|
||||
|
||||
return s, nil
|
||||
}
|
||||
|
||||
func GetSoundFileType(fpath string) SoundType {
|
||||
|
||||
ext := path.Ext(fpath)
|
||||
switch ext {
|
||||
case "mp3":
|
||||
return SoundType_MP3
|
||||
default:
|
||||
return SoundType_Unknown
|
||||
}
|
||||
}
|
||||
|
||||
102
wavy_test.go
102
wavy_test.go
@ -2,7 +2,6 @@ package wavy_test
|
||||
|
||||
import (
|
||||
"io"
|
||||
"math"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
@ -15,97 +14,20 @@ const (
|
||||
channelNum = 2
|
||||
)
|
||||
|
||||
//This is from the Oto example
|
||||
type SineWave struct {
|
||||
freq float64
|
||||
length int64
|
||||
pos int64
|
||||
|
||||
remaining []byte
|
||||
}
|
||||
|
||||
//Implements io.Read interface for SineWave
|
||||
func (s *SineWave) Read(buf []byte) (int, error) {
|
||||
if len(s.remaining) > 0 {
|
||||
n := copy(buf, s.remaining)
|
||||
copy(s.remaining, s.remaining[n:])
|
||||
s.remaining = s.remaining[:len(s.remaining)-n]
|
||||
return n, nil
|
||||
}
|
||||
|
||||
if s.pos == s.length {
|
||||
return 0, io.EOF
|
||||
}
|
||||
|
||||
eof := false
|
||||
if s.pos+int64(len(buf)) > s.length {
|
||||
buf = buf[:s.length-s.pos]
|
||||
eof = true
|
||||
}
|
||||
|
||||
var origBuf []byte
|
||||
if len(buf)%4 > 0 {
|
||||
origBuf = buf
|
||||
buf = make([]byte, len(origBuf)+4-len(origBuf)%4)
|
||||
}
|
||||
|
||||
length := float64(sampleRate) / float64(s.freq)
|
||||
|
||||
num := (bitDepthInBytes) * (channelNum)
|
||||
p := s.pos / int64(num)
|
||||
switch bitDepthInBytes {
|
||||
case 1:
|
||||
for i := 0; i < len(buf)/num; i++ {
|
||||
const max = 127
|
||||
b := int(math.Sin(2*math.Pi*float64(p)/length) * 0.3 * max)
|
||||
for ch := 0; ch < channelNum; ch++ {
|
||||
buf[num*i+ch] = byte(b + 128)
|
||||
}
|
||||
p++
|
||||
}
|
||||
case 2:
|
||||
for i := 0; i < len(buf)/num; i++ {
|
||||
const max = 32767
|
||||
b := int16(math.Sin(2*math.Pi*float64(p)/length) * 0.3 * max)
|
||||
for ch := 0; ch < channelNum; ch++ {
|
||||
buf[num*i+2*ch] = byte(b)
|
||||
buf[num*i+1+2*ch] = byte(b >> 8)
|
||||
}
|
||||
p++
|
||||
}
|
||||
}
|
||||
|
||||
s.pos += int64(len(buf))
|
||||
|
||||
n := len(buf)
|
||||
if origBuf != nil {
|
||||
n = copy(origBuf, buf)
|
||||
s.remaining = buf[n:]
|
||||
}
|
||||
|
||||
if eof {
|
||||
return n, io.EOF
|
||||
}
|
||||
return n, nil
|
||||
}
|
||||
|
||||
func NewSineWave(freq float64, duration time.Duration) *SineWave {
|
||||
l := channelNum * bitDepthInBytes * sampleRate * int64(duration) / int64(time.Second)
|
||||
l = l / 4 * 4
|
||||
return &SineWave{
|
||||
freq: freq,
|
||||
length: l,
|
||||
}
|
||||
}
|
||||
|
||||
func TestSound(t *testing.T) {
|
||||
|
||||
fatihaFilepath := "./test_audio_files/Fatiha.mp3"
|
||||
tadaFilepath := "./test_audio_files/tada.mp3"
|
||||
const fatihaLenMS = 55484
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
//Streaming
|
||||
s, err := wavy.NewSoundStreaming(fatihaFilepath, wavy.SampleRate_44100, wavy.SoundChannelCount_2, wavy.SoundBitDepth_2)
|
||||
s, err := wavy.NewSoundStreaming(fatihaFilepath)
|
||||
if err != nil {
|
||||
t.Errorf("Failed to load streaming sound with path '%s'. Err: %s\n", fatihaFilepath, err)
|
||||
return
|
||||
@ -113,6 +35,7 @@ func TestSound(t *testing.T) {
|
||||
|
||||
s.PlayAsync()
|
||||
time.Sleep(1 * time.Second)
|
||||
s.Player.Pause()
|
||||
|
||||
remTime := s.RemainingTime()
|
||||
if remTime.Milliseconds() >= fatihaLenMS-900 {
|
||||
@ -132,7 +55,7 @@ func TestSound(t *testing.T) {
|
||||
}
|
||||
|
||||
//In-Memory
|
||||
s, err = wavy.NewSoundMem(fatihaFilepath, wavy.SampleRate_44100, wavy.SoundChannelCount_2, wavy.SoundBitDepth_2)
|
||||
s, err = wavy.NewSoundMem(fatihaFilepath)
|
||||
if err != nil {
|
||||
t.Errorf("Failed to load memory sound with path '%s'. Err: %s\n", fatihaFilepath, err)
|
||||
return
|
||||
@ -140,6 +63,7 @@ func TestSound(t *testing.T) {
|
||||
|
||||
s.PlayAsync()
|
||||
time.Sleep(1 * time.Second)
|
||||
s.Player.Pause()
|
||||
|
||||
remTime = s.RemainingTime()
|
||||
if remTime.Milliseconds() >= fatihaLenMS-900 {
|
||||
@ -159,10 +83,14 @@ func TestSound(t *testing.T) {
|
||||
}
|
||||
|
||||
//Memory 'tada.mp3'
|
||||
s, err = wavy.NewSoundMem(tadaFilepath, wavy.SampleRate_44100, wavy.SoundChannelCount_2, wavy.SoundBitDepth_2)
|
||||
s, err = wavy.NewSoundMem(tadaFilepath)
|
||||
if err != nil {
|
||||
t.Errorf("Failed to load memory sound with path '%s'. Err: %s\n", tadaFilepath, err)
|
||||
return
|
||||
}
|
||||
s.PlaySync()
|
||||
|
||||
s.Player.Reset()
|
||||
s.Bytes.Seek(0, io.SeekStart)
|
||||
s.PlaySync()
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user