mirror of
https://github.com/bloeys/wavy.git
synced 2025-12-29 09:28:19 +00:00
Sound info+sound time+remaining time
This commit is contained in:
108
wavy.go
108
wavy.go
@ -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)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
37
wavy_test.go
37
wavy_test.go
@ -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
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user