mirror of
https://github.com/bloeys/wavy.git
synced 2025-12-29 09:28:19 +00:00
Compare commits
9 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| eade35695f | |||
| 3d3cdf1e32 | |||
| 0021d0a7dc | |||
| 2af3583f6f | |||
| daf46e5d41 | |||
| 8f19e800f1 | |||
| aa751bbedd | |||
| 989547c14e | |||
| 2d0f00175a |
16
README.md
16
README.md
@ -29,8 +29,9 @@ If you are using iOS or Linux please check [this](https://github.com/hajimehoshi
|
||||
|
||||
## Supported audio formats
|
||||
|
||||
- MP3
|
||||
- Wav/Wave
|
||||
- MP3 (`.mp3`)
|
||||
- Wav (`.wav`/`.wave`)
|
||||
- OGG (`.ogg`)
|
||||
|
||||
## Usage
|
||||
|
||||
@ -49,8 +50,9 @@ import (
|
||||
|
||||
func main() {
|
||||
|
||||
//At the start if your program you should init wavy and tell it the sampling rate of your sounds (usually 44100),
|
||||
//the number of channels (usually 2) and the number of bytes per channel (usually 2).
|
||||
//At the start if your program you should init wavy and tell it the
|
||||
//sampling rate of your sounds (usually 44100), the number of
|
||||
//channels (usually 2) and the number of bytes per channel (usually 2).
|
||||
//
|
||||
//These settings will be used for all sounds regardless of their actual settings
|
||||
err := wavy.Init(wavy.SampleRate_44100, wavy.SoundChannelCount_2, wavy.SoundBitDepth_2)
|
||||
@ -70,7 +72,8 @@ func main() {
|
||||
mySound.PlaySync()
|
||||
|
||||
//Since the sound finished playing, lets reset to start
|
||||
//by seeking to 0% then play again. Seeking to 0.5 then playing will start from the middle the sound.
|
||||
//by seeking to 0% then play again. Seeking to 0.5 then playing
|
||||
//will start from the middle the sound.
|
||||
mySound.SeekToPercent(0)
|
||||
mySound.PlayAsync()
|
||||
|
||||
@ -131,7 +134,8 @@ mySound.PlaySync()
|
||||
//Set volume to 25%
|
||||
mySound.SetVolume(0.25)
|
||||
|
||||
//Play the sound three times and wait for all 3 plays to finish. Negative numbers will play infinitely till paused
|
||||
//Play the sound three times and wait for all 3 plays to finish.
|
||||
//Negative numbers will play infinitely till paused
|
||||
mySound.LoopAsync(3)
|
||||
mySound.WaitLoop()
|
||||
|
||||
|
||||
6
go.mod
6
go.mod
@ -5,11 +5,13 @@ go 1.18
|
||||
require (
|
||||
github.com/go-audio/wav v1.1.0
|
||||
github.com/hajimehoshi/go-mp3 v0.3.3
|
||||
github.com/hajimehoshi/oto/v2 v2.1.0
|
||||
github.com/hajimehoshi/oto/v2 v2.2.0
|
||||
github.com/jfreymuth/oggvorbis v1.0.3
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/go-audio/audio v1.0.0 // indirect
|
||||
github.com/go-audio/riff v1.0.0 // indirect
|
||||
golang.org/x/sys v0.0.0-20220408201424-a24fb2fb8a0f // indirect
|
||||
github.com/jfreymuth/vorbis v1.0.2 // indirect
|
||||
golang.org/x/sys v0.0.0-20220712014510-0a85c31ab51e // indirect
|
||||
)
|
||||
|
||||
8
go.sum
8
go.sum
@ -9,6 +9,12 @@ github.com/hajimehoshi/go-mp3 v0.3.3/go.mod h1:qMJj/CSDxx6CGHiZeCgbiq2DSUkbK0Ubt
|
||||
github.com/hajimehoshi/oto v0.6.1/go.mod h1:0QXGEkbuJRohbJaxr7ZQSxnju7hEhseiPx2hrh6raOI=
|
||||
github.com/hajimehoshi/oto/v2 v2.1.0 h1:/h+UkbKzhD7xBHOQlWgKUplBPZ+J4DK3P2Y7g2UF1X4=
|
||||
github.com/hajimehoshi/oto/v2 v2.1.0/go.mod h1:9i0oYbpJ8BhVGkXDKdXKfFthX1JUNfXjeTp944W8TGM=
|
||||
github.com/hajimehoshi/oto/v2 v2.2.0 h1:qhTriSacJ/2pdONRa90hjTvpEZH7xIP4W3itwYyE1Uk=
|
||||
github.com/hajimehoshi/oto/v2 v2.2.0/go.mod h1:seWLbgHH7AyUMYKfKYT9pg7PhUu9/SisyJvNTT+ASQo=
|
||||
github.com/jfreymuth/oggvorbis v1.0.3 h1:MLNGGyhOMiVcvea9Dp5+gbs2SAwqwQbtrWnonYa0M0Y=
|
||||
github.com/jfreymuth/oggvorbis v1.0.3/go.mod h1:1U4pqWmghcoVsCJJ4fRBKv9peUJMBHixthRlBeD6uII=
|
||||
github.com/jfreymuth/vorbis v1.0.2 h1:m1xH6+ZI4thH927pgKD8JOH4eaGRm18rEE9/0WKjvNE=
|
||||
github.com/jfreymuth/vorbis v1.0.2/go.mod h1:DoftRo4AznKnShRl1GxiTFCseHr4zR9BN3TWXyuzrqQ=
|
||||
golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
||||
golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js=
|
||||
golang.org/x/mobile v0.0.0-20190415191353-3e0bab5405d6/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o=
|
||||
@ -16,4 +22,6 @@ golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7w
|
||||
golang.org/x/sys v0.0.0-20190429190828-d89cdac9e872/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20220408201424-a24fb2fb8a0f h1:8w7RhxzTVgUzw/AH/9mUV5q0vMgy40SQRursCcfmkCw=
|
||||
golang.org/x/sys v0.0.0-20220408201424-a24fb2fb8a0f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220712014510-0a85c31ab51e h1:NHvCuwuS43lGnYhten69ZWqi2QOj/CiDNcKbVqwVoew=
|
||||
golang.org/x/sys v0.0.0-20220712014510-0a85c31ab51e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
|
||||
62
ogg_streamer.go
Executable file
62
ogg_streamer.go
Executable file
@ -0,0 +1,62 @@
|
||||
package wavy
|
||||
|
||||
import (
|
||||
"io"
|
||||
"os"
|
||||
|
||||
"github.com/jfreymuth/oggvorbis"
|
||||
)
|
||||
|
||||
var _ io.ReadSeeker = &OggStreamer{}
|
||||
|
||||
type OggStreamer struct {
|
||||
F *os.File
|
||||
Dec *oggvorbis.Reader
|
||||
}
|
||||
|
||||
func (ws *OggStreamer) Read(outBuf []byte) (floatsRead int, err error) {
|
||||
|
||||
readerBuf := make([]float32, len(outBuf)/2)
|
||||
floatsRead, err = ws.Dec.Read(readerBuf)
|
||||
F32ToUnsignedPCM16(readerBuf[:floatsRead], outBuf)
|
||||
|
||||
return floatsRead * 2, err
|
||||
}
|
||||
|
||||
func (ws *OggStreamer) Seek(offset int64, whence int) (int64, error) {
|
||||
|
||||
// This is because ogg expects position in samples not bytes
|
||||
offset /= BytesPerSample
|
||||
|
||||
switch whence {
|
||||
case io.SeekStart:
|
||||
if err := ws.Dec.SetPosition(offset); err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
case io.SeekCurrent:
|
||||
|
||||
if err := ws.Dec.SetPosition(ws.Dec.Position() + offset); err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
case io.SeekEnd:
|
||||
if err := ws.Dec.SetPosition(ws.Dec.Length() + offset); err != nil {
|
||||
return 0, err
|
||||
}
|
||||
}
|
||||
|
||||
return ws.Dec.Position() * BytesPerSample, nil
|
||||
}
|
||||
|
||||
// Size returns number of bytes
|
||||
func (ws *OggStreamer) Size() int64 {
|
||||
return ws.Dec.Length() * BytesPerSample
|
||||
}
|
||||
|
||||
func NewOggStreamer(f *os.File, dec *oggvorbis.Reader) *OggStreamer {
|
||||
return &OggStreamer{
|
||||
F: f,
|
||||
Dec: dec,
|
||||
}
|
||||
}
|
||||
@ -5,7 +5,7 @@ import (
|
||||
"io"
|
||||
)
|
||||
|
||||
//Pre-defined errors
|
||||
// Pre-defined errors
|
||||
var (
|
||||
ErrInvalidWhence = errors.New("invalid whence value. Must be: io.SeekStart, io.SeekCurrent, or io.SeekEnd")
|
||||
ErrNegativeSeekPos = errors.New("negative seeker position")
|
||||
@ -16,11 +16,11 @@ var _ io.ReadSeeker = &SoundBuffer{}
|
||||
type SoundBuffer struct {
|
||||
Data []byte
|
||||
|
||||
//Pos is the starting position of the next read
|
||||
// Pos is the starting position of the next read
|
||||
Pos int64
|
||||
}
|
||||
|
||||
//Read only returns io.EOF when bytesRead==0 and no more input is available
|
||||
// Read only returns io.EOF when bytesRead==0 and no more input is available
|
||||
func (sb *SoundBuffer) Read(outBuf []byte) (bytesRead int, err error) {
|
||||
|
||||
bytesRead = copy(outBuf, sb.Data[sb.Pos:])
|
||||
@ -32,10 +32,10 @@ func (sb *SoundBuffer) Read(outBuf []byte) (bytesRead int, err error) {
|
||||
return bytesRead, nil
|
||||
}
|
||||
|
||||
//Seek returns the new position.
|
||||
//An error is only returned if the whence is invalid or if the resulting position is negative.
|
||||
// Seek returns the new position.
|
||||
// An error is only returned if the whence is invalid or if the resulting position is negative.
|
||||
//
|
||||
//If the resulting position is >=len(SoundBuffer.Data) then future Read() calls will return io.EOF
|
||||
// If the resulting position is >=len(SoundBuffer.Data) then future Read() calls will return io.EOF
|
||||
func (sb *SoundBuffer) Seek(offset int64, whence int) (int64, error) {
|
||||
|
||||
newPos := sb.Pos
|
||||
@ -58,10 +58,10 @@ func (sb *SoundBuffer) Seek(offset int64, whence int) (int64, error) {
|
||||
return sb.Pos, nil
|
||||
}
|
||||
|
||||
//Copy returns a new SoundBuffer that uses the same `Data` but with an independent ReadSeeker.
|
||||
//This allows you to have many readers all reading from different positions of the same buffer.
|
||||
// Copy returns a new SoundBuffer that uses the same `Data` but with an independent ReadSeeker.
|
||||
// This allows you to have many readers all reading from different positions of the same buffer.
|
||||
//
|
||||
//The new buffer will have its starting position set to io.SeekStart (`Pos=0`)
|
||||
// The new buffer will have its starting position set to io.SeekStart (`Pos=0`)
|
||||
func (sb *SoundBuffer) Copy() *SoundBuffer {
|
||||
return &SoundBuffer{
|
||||
Data: sb.Data,
|
||||
|
||||
@ -6,6 +6,7 @@ const (
|
||||
SoundType_Unknown SoundType = iota
|
||||
SoundType_MP3
|
||||
SoundType_WAV
|
||||
SoundType_OGG
|
||||
)
|
||||
|
||||
type SampleRate int
|
||||
|
||||
BIN
test_audio_files/camera.mp3
Executable file
BIN
test_audio_files/camera.mp3
Executable file
Binary file not shown.
BIN
test_audio_files/camera.ogg
Executable file
BIN
test_audio_files/camera.ogg
Executable file
Binary file not shown.
83
wav_streamer.go
Executable file
83
wav_streamer.go
Executable file
@ -0,0 +1,83 @@
|
||||
package wavy
|
||||
|
||||
import (
|
||||
"io"
|
||||
"os"
|
||||
|
||||
"github.com/go-audio/wav"
|
||||
)
|
||||
|
||||
var _ io.ReadSeeker = &WavStreamer{}
|
||||
|
||||
type WavStreamer struct {
|
||||
F *os.File
|
||||
Dec *wav.Decoder
|
||||
Pos int64
|
||||
PCMStart int64
|
||||
}
|
||||
|
||||
func (ws *WavStreamer) Read(outBuf []byte) (bytesRead int, err error) {
|
||||
|
||||
bytesRead, err = ws.Dec.PCMChunk.Read(outBuf)
|
||||
ws.Pos += int64(bytesRead)
|
||||
|
||||
return bytesRead, err
|
||||
}
|
||||
|
||||
func (ws *WavStreamer) Seek(offset int64, whence int) (int64, error) {
|
||||
|
||||
// This will only seek the underlying file but not the actual decoder because it can't seek
|
||||
n, err := ws.Dec.Seek(offset, whence)
|
||||
if err != nil {
|
||||
return n, err
|
||||
}
|
||||
|
||||
// Since underlying decoder can't seek back, if the requested movement is back we have to rewind the decoder
|
||||
// then seek forward to the requested position.
|
||||
if n < ws.Pos {
|
||||
|
||||
err = ws.Dec.Rewind()
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
// Anything before PCMStart is not valid sound, so the minimum seek back we allow is PCMStart
|
||||
if n < ws.PCMStart {
|
||||
n = ws.PCMStart
|
||||
} else {
|
||||
n, err = ws.Dec.Seek(offset, whence)
|
||||
if err != nil {
|
||||
return n, err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ws.Pos = n
|
||||
return n, err
|
||||
}
|
||||
|
||||
// Size returns number of bytes
|
||||
func (ws *WavStreamer) Size() int64 {
|
||||
return ws.Dec.PCMLen()
|
||||
}
|
||||
|
||||
func NewWavStreamer(f *os.File, wavDec *wav.Decoder) (*WavStreamer, error) {
|
||||
|
||||
err := wavDec.FwdToPCM()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// The actual data starts somewhat within the file, not at 0
|
||||
currPos, err := wavDec.Seek(0, 1)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &WavStreamer{
|
||||
F: f,
|
||||
Dec: wavDec,
|
||||
Pos: currPos,
|
||||
PCMStart: currPos,
|
||||
}, nil
|
||||
}
|
||||
250
wavy.go
250
wavy.go
@ -5,6 +5,7 @@ import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"math"
|
||||
"os"
|
||||
"path"
|
||||
"time"
|
||||
@ -12,33 +13,34 @@ import (
|
||||
"github.com/go-audio/wav"
|
||||
"github.com/hajimehoshi/go-mp3"
|
||||
"github.com/hajimehoshi/oto/v2"
|
||||
"github.com/jfreymuth/oggvorbis"
|
||||
)
|
||||
|
||||
//SoundInfo contains static info about a loaded sound file
|
||||
// SoundInfo contains static info about a loaded sound file
|
||||
type SoundInfo struct {
|
||||
Type SoundType
|
||||
Mode SoundMode
|
||||
|
||||
//Size is the sound's size in bytes
|
||||
Size int64
|
||||
}
|
||||
|
||||
type Sound struct {
|
||||
Player oto.Player
|
||||
PlayerSeeker io.Seeker
|
||||
Info SoundInfo
|
||||
|
||||
//File 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
|
||||
// File 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
|
||||
File *os.File
|
||||
|
||||
//Data is an io.ReadSeeker over an open file or over a buffer containing the uncompressed sound file.
|
||||
//Becomes nil after close
|
||||
// Data is an io.ReadSeeker over an open file or over a buffer containing the uncompressed sound file.
|
||||
// Becomes nil after close
|
||||
Data io.ReadSeeker
|
||||
|
||||
IsLooping bool
|
||||
}
|
||||
|
||||
//Those values are set after Init
|
||||
// Those values are set after Init
|
||||
var (
|
||||
Ctx *oto.Context
|
||||
|
||||
@ -50,13 +52,13 @@ var (
|
||||
BytesPerSecond int64
|
||||
)
|
||||
|
||||
//Pre-defined errors
|
||||
// Pre-defined errors
|
||||
var (
|
||||
errUnknownSoundType = errors.New("unknown sound type. Sound file extension must be one of: .mp3")
|
||||
)
|
||||
|
||||
//Init prepares the default audio device and does any required setup.
|
||||
//It must be called before loading any sounds
|
||||
// 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))
|
||||
@ -76,30 +78,30 @@ func Init(sr SampleRate, chanCount SoundChannelCount, bitDepth SoundBitDepth) er
|
||||
return nil
|
||||
}
|
||||
|
||||
//Wait blocks until sound finishes playing. If the sound is not playing Wait returns immediately.
|
||||
//In the worst case (Wait sleeping then sound immediately paused), Wait will block ~4% of the total play time.
|
||||
//In most other cases Wait should be accurate to ~1ms.
|
||||
// Wait blocks until sound finishes playing. If the sound is not playing Wait returns immediately.
|
||||
// In the worst case (Wait sleeping then sound immediately paused), Wait will block ~4% of the total play time.
|
||||
// In most other cases Wait should be accurate to ~1ms.
|
||||
//
|
||||
//If you want to wait for all loops to finish then use WaitLoop
|
||||
// If you want to wait for all loops to finish then use WaitLoop
|
||||
func (s *Sound) Wait() {
|
||||
|
||||
if !s.IsPlaying() {
|
||||
return
|
||||
}
|
||||
|
||||
//We wait the remaining time in 25 chunks so that if the sound was paused since wait was called we don't keep blocking
|
||||
// We wait the remaining time in 25 chunks so that if the sound was paused since wait was called we don't keep blocking
|
||||
sleepTime := s.RemainingTime() / 25
|
||||
for s.Player.IsPlaying() {
|
||||
time.Sleep(sleepTime)
|
||||
}
|
||||
|
||||
//If there is anything left it should be tiny so we check frequently
|
||||
// If there is anything left it should be tiny so we check frequently
|
||||
for s.Player.IsPlaying() {
|
||||
time.Sleep(time.Millisecond)
|
||||
}
|
||||
}
|
||||
|
||||
//WaitLoop waits until the sound is no longer looping
|
||||
// WaitLoop waits until the sound is no longer looping
|
||||
func (s *Sound) WaitLoop() {
|
||||
|
||||
for s.IsLooping {
|
||||
@ -107,21 +109,21 @@ func (s *Sound) WaitLoop() {
|
||||
}
|
||||
}
|
||||
|
||||
//PlayAsync plays the sound in the background and returns.
|
||||
// PlayAsync plays the sound in the background and returns.
|
||||
func (s *Sound) PlayAsync() {
|
||||
s.Player.Play()
|
||||
}
|
||||
|
||||
//PlaySync calls PlayAsync() followed by Wait()
|
||||
// PlaySync calls PlayAsync() followed by Wait()
|
||||
func (s *Sound) PlaySync() {
|
||||
s.PlayAsync()
|
||||
s.Wait()
|
||||
}
|
||||
|
||||
//LoopAsync plays the sound 'timesToPlay' times.
|
||||
//If timesToPlay<0 then it is played indefinitely until paused
|
||||
//If timesToPlay==0 then the sound is not played.
|
||||
//If a sound is already playing then it will be paused then resumed in a looping manner
|
||||
// LoopAsync plays the sound 'timesToPlay' times.
|
||||
// If timesToPlay<0 then it is played indefinitely until paused
|
||||
// If timesToPlay==0 then the sound is not played.
|
||||
// If a sound is already playing then it will be paused then resumed in a looping manner
|
||||
func (s *Sound) LoopAsync(timesToPlay int) {
|
||||
|
||||
if timesToPlay == 0 {
|
||||
@ -149,7 +151,7 @@ func (s *Sound) LoopAsync(timesToPlay int) {
|
||||
|
||||
s.Wait()
|
||||
|
||||
//Check is here because we don't want to seek back if we got paused
|
||||
// Check is here because we don't want to seek back if we got paused
|
||||
if !s.IsLooping {
|
||||
break
|
||||
}
|
||||
@ -165,7 +167,7 @@ func (s *Sound) LoopAsync(timesToPlay int) {
|
||||
timesToPlay--
|
||||
s.Wait()
|
||||
|
||||
//Check is here because we don't want to seek back if we got paused
|
||||
// Check is here because we don't want to seek back if we got paused
|
||||
if !s.IsLooping {
|
||||
break
|
||||
}
|
||||
@ -179,14 +181,14 @@ func (s *Sound) LoopAsync(timesToPlay int) {
|
||||
}()
|
||||
}
|
||||
|
||||
//TotalTime returns the time taken to play the entire sound.
|
||||
//Safe to use after close
|
||||
// TotalTime returns the time taken to play the entire sound.
|
||||
// Safe to use after close
|
||||
func (s *Sound) TotalTime() time.Duration {
|
||||
return PlayTimeFromByteCount(s.Info.Size)
|
||||
}
|
||||
|
||||
//RemainingTime returns the time left in the clip, which is affected by pausing/resetting/seeking of the sound.
|
||||
//Returns zero after close
|
||||
// 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() {
|
||||
@ -199,8 +201,8 @@ func (s *Sound) RemainingTime() time.Duration {
|
||||
return PlayTimeFromByteCount(s.Info.Size - currBytePos)
|
||||
}
|
||||
|
||||
//SetVolume must be between 0 and 1 (both inclusive). Other values will panic.
|
||||
//The default volume is 1.
|
||||
// SetVolume must be between 0 and 1 (both inclusive). Other values will panic.
|
||||
// The default volume is 1.
|
||||
func (s *Sound) SetVolume(newVol float64) {
|
||||
|
||||
if newVol < 0 || newVol > 1 {
|
||||
@ -210,7 +212,7 @@ func (s *Sound) SetVolume(newVol float64) {
|
||||
s.Player.SetVolume(newVol)
|
||||
}
|
||||
|
||||
//Volume returns the current volume
|
||||
// Volume returns the current volume
|
||||
func (s *Sound) Volume() float64 {
|
||||
return s.Player.Volume()
|
||||
}
|
||||
@ -224,34 +226,26 @@ func (s *Sound) IsPlaying() bool {
|
||||
return s.Player.IsPlaying()
|
||||
}
|
||||
|
||||
//SeekToPercent moves the current position of the sound to the given percentage of the total sound length.
|
||||
//For example, if a sound is 10s long and percent=0.5 then when the sound is played it will start from 5s.
|
||||
// SeekToPercent moves the current position of the sound to the given percentage of the total sound length.
|
||||
// For example, if a sound is 10s long and percent=0.5 then when the sound is played it will start from 5s.
|
||||
//
|
||||
//This can be used while the sound is playing.
|
||||
// This can be used while the sound is playing.
|
||||
//
|
||||
//percent is clamped [0,1], so passing <0 is the same as zero, and >1 is the same as 1
|
||||
// percent is clamped [0,1], so passing <0 is the same as zero, and >1 is the same as 1
|
||||
func (s *Sound) SeekToPercent(percent float64) {
|
||||
|
||||
if !s.IsPlaying() {
|
||||
s.Player.Reset()
|
||||
}
|
||||
|
||||
percent = clamp01F64(percent)
|
||||
s.Data.Seek(int64(float64(s.Info.Size)*percent), io.SeekStart)
|
||||
s.PlayerSeeker.Seek(int64(float64(s.Info.Size)*percent), io.SeekStart)
|
||||
}
|
||||
|
||||
//SeekToTime moves the current position of the sound to the given duration.
|
||||
//For example if you use t=5*time.Second then play you will start from 5th second.
|
||||
// SeekToTime moves the current position of the sound to the given duration.
|
||||
// For example if you use t=5*time.Second then play you will start from 5th second.
|
||||
//
|
||||
//This can be used while the sound is playing.
|
||||
// This can be used while the sound is playing.
|
||||
//
|
||||
//t is clamped between [0, totalTime]
|
||||
// t is clamped between [0, totalTime]
|
||||
func (s *Sound) SeekToTime(t time.Duration) {
|
||||
|
||||
if !s.IsPlaying() {
|
||||
s.Player.Reset()
|
||||
}
|
||||
|
||||
byteCount := ByteCountFromPlayTime(t)
|
||||
if byteCount < 0 {
|
||||
byteCount = 0
|
||||
@ -259,15 +253,15 @@ func (s *Sound) SeekToTime(t time.Duration) {
|
||||
byteCount = s.Info.Size
|
||||
}
|
||||
|
||||
s.Data.Seek(byteCount, io.SeekStart)
|
||||
s.PlayerSeeker.Seek(byteCount, io.SeekStart)
|
||||
}
|
||||
|
||||
func (s *Sound) IsClosed() bool {
|
||||
return s.Data == nil
|
||||
}
|
||||
|
||||
//Close will clean underlying resources, and the 'Ctx' and 'Bytes' fields will be made nil.
|
||||
//Repeated calls are no-ops
|
||||
// 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 {
|
||||
|
||||
if s.IsClosed() {
|
||||
@ -297,10 +291,10 @@ func (s *Sound) Close() error {
|
||||
return fdErr
|
||||
}
|
||||
|
||||
//CopyInMemSound returns a new sound object that has identitcal info and uses the same underlying data, but with independent play controls (e.g. one playing at the start while one is in the middle).
|
||||
//Since the sound data is not copied this function is very fast.
|
||||
// CopyInMemSound returns a new sound object that has identitcal info and uses the same underlying data, but with independent play controls (e.g. one playing at the start while one is in the middle).
|
||||
// Since the sound data is not copied this function is very fast.
|
||||
//
|
||||
//Panics if the sound is not in-memory
|
||||
// Panics if the sound is not in-memory
|
||||
func CopyInMemSound(s *Sound) *Sound {
|
||||
|
||||
if s.Info.Mode != SoundMode_Memory {
|
||||
@ -314,14 +308,15 @@ func CopyInMemSound(s *Sound) *Sound {
|
||||
|
||||
return &Sound{
|
||||
Player: p,
|
||||
PlayerSeeker: p.(io.Seeker),
|
||||
File: nil,
|
||||
Data: sb,
|
||||
Info: s.Info,
|
||||
}
|
||||
}
|
||||
|
||||
//ClipInMemSoundPercent is like CopyInMemSound but produces a sound that plays only between from and to.
|
||||
//fromPercent and toPercent must be between 0 and 1
|
||||
// ClipInMemSoundPercent is like CopyInMemSound but produces a sound that plays only between from and to.
|
||||
// fromPercent and toPercent must be between 0 and 1
|
||||
func ClipInMemSoundPercent(s *Sound, fromPercent, toPercent float64) *Sound {
|
||||
|
||||
if s.Info.Mode != SoundMode_Memory {
|
||||
@ -342,6 +337,7 @@ func ClipInMemSoundPercent(s *Sound, fromPercent, toPercent float64) *Sound {
|
||||
|
||||
return &Sound{
|
||||
Player: p,
|
||||
PlayerSeeker: p.(io.Seeker),
|
||||
File: nil,
|
||||
Data: sb,
|
||||
Info: s.Info,
|
||||
@ -356,8 +352,8 @@ func ResumeAllSounds() {
|
||||
Ctx.Resume()
|
||||
}
|
||||
|
||||
//NewSoundStreaming plays sound by streaming from a file, so no need to load the entire file into memory.
|
||||
//Good for large sound files
|
||||
// NewSoundStreaming plays sound by streaming from a file, so no need to load the entire file into memory.
|
||||
// Good for large sound files
|
||||
func NewSoundStreaming(fpath string) (s *Sound, err error) {
|
||||
|
||||
soundType := GetSoundFileType(fpath)
|
||||
@ -365,7 +361,7 @@ func NewSoundStreaming(fpath string) (s *Sound, err error) {
|
||||
return nil, errUnknownSoundType
|
||||
}
|
||||
|
||||
//We read file but don't close so the player can stream the file any time later
|
||||
// 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
|
||||
@ -379,7 +375,7 @@ func NewSoundStreaming(fpath string) (s *Sound, err error) {
|
||||
},
|
||||
}
|
||||
|
||||
err = soundFromReaderSeeker(file, s)
|
||||
err = soundFromFile(file, s)
|
||||
if err != nil {
|
||||
return nil, getLoadingErr(fpath, err)
|
||||
}
|
||||
@ -387,7 +383,53 @@ func NewSoundStreaming(fpath string) (s *Sound, err error) {
|
||||
return s, nil
|
||||
}
|
||||
|
||||
//NewSoundMem loads the entire sound file into memory
|
||||
func soundFromFile(f *os.File, s *Sound) error {
|
||||
|
||||
if s.Info.Type == SoundType_MP3 {
|
||||
|
||||
dec, err := mp3.NewDecoder(f)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
s.Data = dec
|
||||
s.Player = Ctx.NewPlayer(dec)
|
||||
s.PlayerSeeker = s.Player.(io.Seeker)
|
||||
s.Info.Size = dec.Length()
|
||||
} else if s.Info.Type == SoundType_WAV {
|
||||
|
||||
ws, err := NewWavStreamer(f, wav.NewDecoder(f))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
s.Data = ws
|
||||
s.Player = Ctx.NewPlayer(ws)
|
||||
s.PlayerSeeker = s.Player.(io.Seeker)
|
||||
s.Info.Size = ws.Size()
|
||||
} else if s.Info.Type == SoundType_OGG {
|
||||
|
||||
oggReader, err := oggvorbis.NewReader(f)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
oggStreamer := NewOggStreamer(f, oggReader)
|
||||
|
||||
s.Data = oggStreamer
|
||||
s.Player = Ctx.NewPlayer(oggStreamer)
|
||||
s.PlayerSeeker = s.Player.(io.Seeker)
|
||||
s.Info.Size = oggStreamer.Size()
|
||||
}
|
||||
|
||||
if s.Data == nil {
|
||||
panic("invalid sound type. This is probably a bug!")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// NewSoundMem loads the entire sound file into memory
|
||||
func NewSoundMem(fpath string) (s *Sound, err error) {
|
||||
|
||||
soundType := GetSoundFileType(fpath)
|
||||
@ -408,7 +450,7 @@ func NewSoundMem(fpath string) (s *Sound, err error) {
|
||||
},
|
||||
}
|
||||
|
||||
err = soundFromReaderSeeker(bytesReader, s)
|
||||
err = decodeSoundFromReaderSeeker(bytesReader, s)
|
||||
if err != nil {
|
||||
return nil, getLoadingErr(fpath, err)
|
||||
}
|
||||
@ -420,7 +462,9 @@ func getLoadingErr(fpath string, err error) error {
|
||||
return fmt.Errorf("failed to load '%s' with err '%s'", fpath, err.Error())
|
||||
}
|
||||
|
||||
func soundFromReaderSeeker(r io.ReadSeeker, s *Sound) error {
|
||||
// decodeSoundFromReaderSeeker reads and decodes till EOF, and places the final
|
||||
// PCM16 data in a buffer, thus producing an in-memory sound
|
||||
func decodeSoundFromReaderSeeker(r io.ReadSeeker, s *Sound) error {
|
||||
|
||||
if s.Info.Type == SoundType_MP3 {
|
||||
|
||||
@ -437,9 +481,8 @@ func soundFromReaderSeeker(r io.ReadSeeker, s *Sound) error {
|
||||
sb := &SoundBuffer{Data: finalBuf}
|
||||
s.Data = sb
|
||||
s.Player = Ctx.NewPlayer(sb)
|
||||
s.PlayerSeeker = s.Player.(io.Seeker)
|
||||
s.Info.Size = int64(len(sb.Data))
|
||||
return nil
|
||||
|
||||
} else if s.Info.Type == SoundType_WAV {
|
||||
|
||||
wavDec := wav.NewDecoder(r)
|
||||
@ -456,11 +499,27 @@ func soundFromReaderSeeker(r io.ReadSeeker, s *Sound) error {
|
||||
sb := &SoundBuffer{Data: finalBuf}
|
||||
s.Data = sb
|
||||
s.Player = Ctx.NewPlayer(sb)
|
||||
s.PlayerSeeker = s.Player.(io.Seeker)
|
||||
s.Info.Size = int64(len(sb.Data))
|
||||
return nil
|
||||
} else if s.Info.Type == SoundType_OGG {
|
||||
|
||||
soundData, _, err := oggvorbis.ReadAll(r)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
panic("invalid sound type")
|
||||
sb := &SoundBuffer{Data: F32ToUnsignedPCM16(soundData, nil)}
|
||||
s.Data = sb
|
||||
s.Player = Ctx.NewPlayer(sb)
|
||||
s.PlayerSeeker = s.Player.(io.Seeker)
|
||||
s.Info.Size = int64(len(sb.Data))
|
||||
}
|
||||
|
||||
if s.Data == nil {
|
||||
panic("invalid sound type. This is probably a bug!")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func GetSoundFileType(fpath string) SoundType {
|
||||
@ -471,22 +530,24 @@ func GetSoundFileType(fpath string) SoundType {
|
||||
return SoundType_MP3
|
||||
case ".wav", ".wave":
|
||||
return SoundType_WAV
|
||||
case ".ogg":
|
||||
return SoundType_OGG
|
||||
default:
|
||||
return SoundType_Unknown
|
||||
}
|
||||
}
|
||||
|
||||
//ReadAllFromReader takes an io.Reader and reads until error or io.EOF.
|
||||
// ReadAllFromReader takes an io.Reader and reads until error or io.EOF.
|
||||
//
|
||||
//If io.EOF is reached then read bytes are returned with a nil error.
|
||||
//If the reader returns an error that's not io.EOF then everything read till that point is returned along with the error
|
||||
// If io.EOF is reached then read bytes are returned with a nil error.
|
||||
// If the reader returns an error that's not io.EOF then everything read till that point is returned along with the error
|
||||
//
|
||||
//readingBufSize is the buffer used to read from reader.Read(). Bigger values might read more efficiently.
|
||||
//If readingBufSize<4096 then readingBufSize is set to 4096
|
||||
// readingBufSize is the buffer used to read from reader.Read(). Bigger values might read more efficiently.
|
||||
// If readingBufSize<4096 then readingBufSize is set to 4096
|
||||
//
|
||||
//ouputBufSize is used to set the capacity of the final buffer to be returned. This can greatly improve performance
|
||||
//if you know the size of the output. It is allowed to have an outputBufSize that's smaller or larger than what the reader
|
||||
//ends up returning
|
||||
// ouputBufSize is used to set the capacity of the final buffer to be returned. This can greatly improve performance
|
||||
// if you know the size of the output. It is allowed to have an outputBufSize that's smaller or larger than what the reader
|
||||
// ends up returning
|
||||
func ReadAllFromReader(reader io.Reader, readingBufSize, ouputBufSize uint64) ([]byte, error) {
|
||||
|
||||
if readingBufSize < 4096 {
|
||||
@ -509,19 +570,19 @@ func ReadAllFromReader(reader io.Reader, readingBufSize, ouputBufSize uint64) ([
|
||||
}
|
||||
}
|
||||
|
||||
//PlayTimeFromByteCount returns the time taken to play this many bytes
|
||||
// PlayTimeFromByteCount returns the time taken to play this many bytes
|
||||
func PlayTimeFromByteCount(byteCount int64) time.Duration {
|
||||
//timeToPlayInMs = timeToPlayInSec * 1000 = byteCount / bytesPerSecond * 1000
|
||||
// timeToPlayInMs = timeToPlayInSec * 1000 = byteCount / bytesPerSecond * 1000
|
||||
lenInMs := float64(byteCount) / float64(BytesPerSecond) * 1000
|
||||
return time.Duration(lenInMs) * time.Millisecond
|
||||
}
|
||||
|
||||
//PlayTimeFromByteCount returns how many bytes are needed to produce a sound that takes t time to play
|
||||
// PlayTimeFromByteCount returns how many bytes are needed to produce a sound that takes t time to play
|
||||
func ByteCountFromPlayTime(t time.Duration) int64 {
|
||||
return t.Milliseconds() * BytesPerSecond / 1000
|
||||
}
|
||||
|
||||
//clampF64 [min,max]
|
||||
// clampF64 [min,max]
|
||||
func clamp01F64(x float64) float64 {
|
||||
|
||||
if x < 0 {
|
||||
@ -534,3 +595,32 @@ func clamp01F64(x float64) float64 {
|
||||
|
||||
return x
|
||||
}
|
||||
|
||||
// F32ToUnsignedPCM16 takes PCM data stored as float32 between [-1, 1]
|
||||
// and returns a byte array of uint16, where each two subsequent bytes represent one uint16.
|
||||
func F32ToUnsignedPCM16(fs []float32, outBuf []byte) []byte {
|
||||
|
||||
if outBuf == nil {
|
||||
outBuf = make([]byte, len(fs)*2)
|
||||
}
|
||||
|
||||
for i := 0; i < len(fs); i++ {
|
||||
|
||||
// Remap [-1,1]->[-32768, 32767], then re-interprets the int16 as a uint16.
|
||||
// With this, the negative values are mapped into the higher half of the uint16 range,
|
||||
// while positive values remain unchanged
|
||||
x := fs[i]
|
||||
var u16 uint16
|
||||
if x < 0 {
|
||||
u16 = uint16(x * -math.MinInt16)
|
||||
} else {
|
||||
u16 = uint16(x * math.MaxInt16)
|
||||
}
|
||||
|
||||
baseIndex := i * 2
|
||||
outBuf[baseIndex] = byte(u16 >> 0)
|
||||
outBuf[baseIndex+1] = byte(u16 >> 8)
|
||||
}
|
||||
|
||||
return outBuf
|
||||
}
|
||||
|
||||
64
wavy_test.go
64
wavy_test.go
@ -7,19 +7,29 @@ import (
|
||||
"github.com/bloeys/wavy"
|
||||
)
|
||||
|
||||
func TestSound(t *testing.T) {
|
||||
func TestWavy(t *testing.T) {
|
||||
t.Run("Init", InitSubtest)
|
||||
t.Run("MP3", MP3Subtest)
|
||||
t.Run("Wav", WavSubtest)
|
||||
t.Run("Ogg", OggSubtest)
|
||||
}
|
||||
|
||||
func InitSubtest(t *testing.T) {
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
func MP3Subtest(t *testing.T) {
|
||||
|
||||
const fatihaFilepath = "./test_audio_files/Fatiha.mp3"
|
||||
const tadaFilepath = "./test_audio_files/tada.mp3"
|
||||
const fatihaLenMS = 55484
|
||||
|
||||
//Streaming
|
||||
// Mp3 streaming
|
||||
s, err := wavy.NewSoundStreaming(fatihaFilepath)
|
||||
if err != nil {
|
||||
t.Errorf("Failed to load streaming sound with path '%s'. Err: %s\n", fatihaFilepath, err)
|
||||
@ -47,7 +57,7 @@ func TestSound(t *testing.T) {
|
||||
return
|
||||
}
|
||||
|
||||
//In-Memory
|
||||
// Mp3 in-memory
|
||||
s, err = wavy.NewSoundMem(fatihaFilepath)
|
||||
if err != nil {
|
||||
t.Errorf("Failed to load memory sound with path '%s'. Err: %s\n", fatihaFilepath, err)
|
||||
@ -75,7 +85,7 @@ func TestSound(t *testing.T) {
|
||||
return
|
||||
}
|
||||
|
||||
//Memory 'tada.mp3'
|
||||
// 'tada.mp3' memory
|
||||
s, err = wavy.NewSoundMem(tadaFilepath)
|
||||
if err != nil {
|
||||
t.Errorf("Failed to load memory sound with path '%s'. Err: %s\n", tadaFilepath, err)
|
||||
@ -83,17 +93,17 @@ func TestSound(t *testing.T) {
|
||||
}
|
||||
s.PlaySync()
|
||||
|
||||
//Test repeat playing
|
||||
// Test repeat playing
|
||||
s2 := wavy.CopyInMemSound(s)
|
||||
s2.SetVolume(0.25)
|
||||
|
||||
//Already finished, should not play
|
||||
// Already finished, should not play
|
||||
s.PlaySync()
|
||||
|
||||
//Should play from beginning
|
||||
// Should play from beginning
|
||||
s2.PlaySync()
|
||||
|
||||
//Test seek and play
|
||||
// Test seek and play
|
||||
s2.SeekToPercent(0.2)
|
||||
s2.PlaySync()
|
||||
|
||||
@ -103,15 +113,49 @@ func TestSound(t *testing.T) {
|
||||
s3 := wavy.ClipInMemSoundPercent(s2, 0, 0.25)
|
||||
s3.LoopAsync(3)
|
||||
s3.WaitLoop()
|
||||
}
|
||||
|
||||
func WavSubtest(t *testing.T) {
|
||||
|
||||
//Wav
|
||||
const wavFPath = "./test_audio_files/camera.wav"
|
||||
s, err = wavy.NewSoundMem(wavFPath)
|
||||
|
||||
s, err := wavy.NewSoundMem(wavFPath)
|
||||
if err != nil {
|
||||
t.Errorf("Failed to load memory sound with path '%s'. Err: %s\n", wavFPath, err)
|
||||
return
|
||||
}
|
||||
s.PlaySync()
|
||||
|
||||
// Wav streaming
|
||||
s, err = wavy.NewSoundStreaming(wavFPath)
|
||||
if err != nil {
|
||||
t.Errorf("Failed to load streaming sound with path '%s'. Err: %s\n", wavFPath, err)
|
||||
return
|
||||
}
|
||||
s.PlaySync()
|
||||
s.SeekToPercent(0.5)
|
||||
s.PlaySync()
|
||||
}
|
||||
|
||||
func OggSubtest(t *testing.T) {
|
||||
|
||||
const oggFPath = "./test_audio_files/camera.ogg"
|
||||
s, err := wavy.NewSoundMem(oggFPath)
|
||||
if err != nil {
|
||||
t.Errorf("Failed to load memory sound with path '%s'. Err: %s\n", oggFPath, err)
|
||||
return
|
||||
}
|
||||
s.PlaySync()
|
||||
|
||||
// Ogg streaming
|
||||
s, err = wavy.NewSoundStreaming(oggFPath)
|
||||
if err != nil {
|
||||
t.Errorf("Failed to load streaming sound with path '%s'. Err: %s\n", oggFPath, err)
|
||||
return
|
||||
}
|
||||
s.PlaySync()
|
||||
s.SeekToPercent(.5)
|
||||
s.PlaySync()
|
||||
}
|
||||
|
||||
func TestByteCountFromPlayTime(t *testing.T) {
|
||||
|
||||
Reference in New Issue
Block a user