Sound info+sound time+remaining time

This commit is contained in:
bloeys
2022-06-18 13:26:28 +04:00
parent dd0c21efc7
commit a0af1959e0
2 changed files with 133 additions and 12 deletions

108
wavy.go
View File

@ -4,6 +4,7 @@ import (
"bytes" "bytes"
"errors" "errors"
"fmt" "fmt"
"io"
"os" "os"
"strings" "strings"
"time" "time"
@ -40,20 +41,44 @@ const (
SoundBitDepth_2 SoundBitDepth = 2 SoundBitDepth_2 SoundBitDepth = 2
) )
type SoundMode int
const (
SoundMode_Streaming SoundMode = iota
SoundMode_Memory
)
var ( var (
ErrunknownSoundType = errors.New("unknown sound type. Sound file extensions must be: .mp3") 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 { type Sound struct {
//Becomes nil after close
Ctx *oto.Context Ctx *oto.Context
Player oto.Player 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 FileDesc *os.File
//BytesReader is a reader from a buffer containing the entire sound file //Bytes is an io.ReadSeeker over an open file or over a buffer containing the uncompressed sound file.
BytesReader *bytes.Reader //Becomes nil after close
Bytes io.ReadSeeker
Info SoundInfo
} }
func (s *Sound) PlayAsync() { func (s *Sound) PlayAsync() {
@ -63,19 +88,61 @@ func (s *Sound) PlayAsync() {
func (s *Sound) PlaySync() { func (s *Sound) PlaySync() {
s.Player.Play() s.Player.Play()
time.Sleep(s.TotalTime())
//Should never run, but just in case TotalTimeMS was a bit inaccurate
for s.Player.IsPlaying() { 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 { func (s *Sound) Close() error {
if s.IsClosed() {
return nil
}
var fdErr error = nil var fdErr error = nil
if s.FileDesc != nil { if s.FileDesc != nil {
fdErr = s.FileDesc.Close() fdErr = s.FileDesc.Close()
} }
s.Ctx = nil
s.Bytes = nil
playerErr := s.Player.Close() playerErr := s.Player.Close()
if playerErr == nil && fdErr == nil { if playerErr == nil && fdErr == nil {
return nil return nil
} }
@ -117,8 +184,20 @@ func NewSoundStreaming(fpath string, sr SampleRate, chanCount SoundChannelCount,
return nil, err 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 //Load file depending on type
s = &Sound{Ctx: otoCtx, Type: soundType, FileDesc: file}
if soundType == SoundType_MP3 { if soundType == SoundType_MP3 {
dec, err := mp3.NewDecoder(file) dec, err := mp3.NewDecoder(file)
@ -126,7 +205,9 @@ func NewSoundStreaming(fpath string, sr SampleRate, chanCount SoundChannelCount,
return nil, err return nil, err
} }
s.Info.Size = dec.Length()
s.Player = otoCtx.NewPlayer(dec) s.Player = otoCtx.NewPlayer(dec)
s.Bytes = dec
} }
return s, nil return s, nil
@ -156,10 +237,21 @@ func NewSoundMem(fpath string, sr SampleRate, chanCount SoundChannelCount, bitDe
if err != nil { if err != nil {
return nil, err return nil, err
} }
bytesReader := bytes.NewReader(fileBytes) 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 //Load file depending on type
s = &Sound{Ctx: otoCtx, Type: soundType, BytesReader: bytesReader}
if soundType == SoundType_MP3 { if soundType == SoundType_MP3 {
dec, err := mp3.NewDecoder(bytesReader) dec, err := mp3.NewDecoder(bytesReader)
@ -167,6 +259,8 @@ func NewSoundMem(fpath string, sr SampleRate, chanCount SoundChannelCount, bitDe
return nil, err return nil, err
} }
s.Bytes = dec
s.Info.Size = dec.Length()
s.Player = otoCtx.NewPlayer(dec) s.Player = otoCtx.NewPlayer(dec)
} }

View File

@ -100,33 +100,60 @@ func NewSineWave(freq float64, duration time.Duration) *SineWave {
func TestSound(t *testing.T) { func TestSound(t *testing.T) {
audioFPath := "./test_audio_files/Fatiha.mp3" fatihaFilepath := "./test_audio_files/Fatiha.mp3"
const fatihaLenMS = 55484
//Streaming //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 { 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 return
} }
s.PlayAsync() s.PlayAsync()
time.Sleep(1 * time.Second) 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 { if err := s.Close(); err != nil {
t.Errorf("Closing streaming sound failed. Err: %s\n", err) t.Errorf("Closing streaming sound failed. Err: %s\n", err)
return 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 //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 { 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 return
} }
s.PlayAsync() s.PlayAsync()
time.Sleep(1 * time.Second) 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 { if err := s.Close(); err != nil {
t.Errorf("Closing in-memory sound failed. Err: %s\n", err) t.Errorf("Closing in-memory sound failed. Err: %s\n", err)
return 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
}
} }