From eb6598c34ff63b82c3540d0f57aed0fde203d79f Mon Sep 17 00:00:00 2001 From: Mark Kremer Date: Wed, 1 Sep 2021 16:18:20 +0200 Subject: [PATCH] First draft for new speaker with Oto v2 --- go.mod | 2 +- go.sum | 11 +- speaker/speaker.go | 251 ++++++++++++++++++++++++++++++--------------- 3 files changed, 171 insertions(+), 93 deletions(-) diff --git a/go.mod b/go.mod index b6404dc..5539867 100644 --- a/go.mod +++ b/go.mod @@ -5,7 +5,7 @@ go 1.14 require ( github.com/gdamore/tcell v1.3.0 github.com/hajimehoshi/go-mp3 v0.3.0 - github.com/hajimehoshi/oto v0.7.1 + github.com/hajimehoshi/oto/v2 v2.0.0 github.com/jfreymuth/oggvorbis v1.0.1 github.com/mewkiz/flac v1.0.7 github.com/pkg/errors v0.9.1 diff --git a/go.sum b/go.sum index a001a2b..ec2d043 100644 --- a/go.sum +++ b/go.sum @@ -12,10 +12,8 @@ github.com/hajimehoshi/go-mp3 v0.3.0 h1:fTM5DXjp/DL2G74HHAs/aBGiS9Tg7wnp+jkU38bH github.com/hajimehoshi/go-mp3 v0.3.0/go.mod h1:qMJj/CSDxx6CGHiZeCgbiq2DSUkbK0UbtXShQcnfyMM= github.com/hajimehoshi/oto v0.6.1 h1:7cJz/zRQV4aJvMSSRqzN2TImoVVMpE0BCY4nrNJaDOM= github.com/hajimehoshi/oto v0.6.1/go.mod h1:0QXGEkbuJRohbJaxr7ZQSxnju7hEhseiPx2hrh6raOI= -github.com/hajimehoshi/oto v0.6.3 h1:NfrHdINv+7J8JhfkbHBROlWCzFSWc9PaHm2lS90KNzY= -github.com/hajimehoshi/oto v0.6.3/go.mod h1:0QXGEkbuJRohbJaxr7ZQSxnju7hEhseiPx2hrh6raOI= -github.com/hajimehoshi/oto v0.7.1 h1:I7maFPz5MBCwiutOrz++DLdbr4rTzBsbBuV2VpgU9kk= -github.com/hajimehoshi/oto v0.7.1/go.mod h1:wovJ8WWMfFKvP587mhHgot/MBr4DnNy9m6EepeVGnos= +github.com/hajimehoshi/oto/v2 v2.0.0 h1:eSkhgJGFjaNG03vLonB98rnd6FkvtglxEOrXKuxR8Po= +github.com/hajimehoshi/oto/v2 v2.0.0/go.mod h1:rUKQmwMkqmRxe+IAof9+tuYA2ofm8cAWXFmSfzDN8vQ= github.com/icza/bitio v1.0.0 h1:squ/m1SHyFeCA6+6Gyol1AxV9nmPPlJFT8c2vKdj3U8= github.com/icza/bitio v1.0.0/go.mod h1:0jGnlLAx8MKMr9VGnn/4YrvZiprkvBelsVIbA9Jjr9A= github.com/icza/mighty v0.0.0-20180919140131-cfd07d671de6 h1:8UsGZ2rr2ksmEru6lToqnXgA8Mz1DP11X4zSJ159C3k= @@ -32,20 +30,15 @@ github.com/mewkiz/flac v1.0.7 h1:uIXEjnuXqdRaZttmSFM5v5Ukp4U6orrZsnYGGR3yow8= github.com/mewkiz/flac v1.0.7/go.mod h1:yU74UH277dBUpqxPouHSQIar3G1X/QIclVbFahSd1pU= github.com/mewkiz/pkg v0.0.0-20190919212034-518ade7978e2 h1:EyTNMdePWaoWsRSGQnXiSoQu0r6RS1eA557AwJhlzHU= github.com/mewkiz/pkg v0.0.0-20190919212034-518ade7978e2/go.mod h1:3E2FUC/qYUfM8+r9zAwpeHJzqRVVMIYnpzD/clwWxyA= -github.com/pkg/errors v0.8.1 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I= github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= -golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8 h1:idBdZTd9UioThJp8KpM/rTSinK/ChZFBE43/WtIy8zg= golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/image v0.0.0-20190220214146-31aff87c08e9/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js= -golang.org/x/image v0.0.0-20190227222117-0694c2d4d067 h1:KYGJGHOQy8oSi1fDlSpcZF0+juKwk/hEMv5SiwHogR0= golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js= -golang.org/x/mobile v0.0.0-20190415191353-3e0bab5405d6 h1:vyLBGJPIl9ZYbcQFM2USFmJBK6KI+t+z6jL0lbwjrnc= golang.org/x/mobile v0.0.0-20190415191353-3e0bab5405d6/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o= golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190429190828-d89cdac9e872 h1:cGjJzUd8RgBw428LXP65YXni0aiGNA4Bl+ls8SmLOm8= golang.org/x/sys v0.0.0-20190429190828-d89cdac9e872/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190626150813-e07cf5db2756 h1:9nuHUbU8dRnRRfj9KjWUVrJeoexdbeMjttk6Oh1rD10= golang.org/x/sys v0.0.0-20190626150813-e07cf5db2756/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= diff --git a/speaker/speaker.go b/speaker/speaker.go index d59389f..f8cb3d7 100644 --- a/speaker/speaker.go +++ b/speaker/speaker.go @@ -2,116 +2,127 @@ package speaker import ( - "sync" - "github.com/faiface/beep" - "github.com/hajimehoshi/oto" + "github.com/hajimehoshi/oto/v2" "github.com/pkg/errors" + "io" + "time" ) +const channelNum = 2 +const bitDepthInBytes = 2 +const bytesPerSample = bitDepthInBytes * channelNum + var ( - mu sync.Mutex - mixer beep.Mixer - samples [][2]float64 - buf []byte - context *oto.Context - player *oto.Player - done chan struct{} + context *oto.Context + contextSampleRate beep.SampleRate ) // Init initializes audio playback through speaker. Must be called before using this package. -// -// The bufferSize argument specifies the number of samples of the speaker's buffer. Bigger -// bufferSize means lower CPU usage and more reliable playback. Lower bufferSize means better -// responsiveness and less delay. -func Init(sampleRate beep.SampleRate, bufferSize int) error { - mu.Lock() - defer mu.Unlock() - - Close() - - mixer = beep.Mixer{} - - numBytes := bufferSize * 4 - samples = make([][2]float64, bufferSize) - buf = make([]byte, numBytes) +func Init(sampleRate beep.SampleRate) error { + if context != nil { + return errors.New("speaker is already initialized") + } var err error - context, err = oto.NewContext(int(sampleRate), 2, 2, numBytes) + var ready chan struct{} + context, ready, err = oto.NewContext(int(sampleRate), channelNum, bitDepthInBytes) if err != nil { return errors.Wrap(err, "failed to initialize speaker") } - player = context.NewPlayer() + <-ready - done = make(chan struct{}) - - go func() { - for { - select { - default: - update() - case <-done: - return - } - } - }() + contextSampleRate = sampleRate return nil } -// Close closes the playback and the driver. In most cases, there is certainly no need to call Close -// even when the program doesn't play anymore, because in properly set systems, the default mixer -// handles multiple concurrent processes. It's only when the default device is not a virtual but hardware -// device, that you'll probably want to manually manage the device from your application. -func Close() { - if player != nil { - if done != nil { - done <- struct{}{} - done = nil - } - player.Close() - context.Close() - player = nil - } -} - -// Lock locks the speaker. While locked, speaker won't pull new data from the playing Stramers. Lock -// if you want to modify any currently playing Streamers to avoid race conditions. +// Suspend audio playback until Resume() is called. // -// Always lock speaker for as little time as possible, to avoid playback glitches. -func Lock() { - mu.Lock() +// Suspend is concurrent-safe. +func Suspend() error { + err := context.Suspend() + return errors.Wrap(err, "failed to suspend speaker") } -// Unlock unlocks the speaker. Call after modifying any currently playing Streamer. -func Unlock() { - mu.Unlock() +// Resume audio playback after it was suspended by Suspend(). +// +// Resume is concurrent-safe. +func Resume() error { + err := context.Resume() + return errors.Wrap(err, "failed to resume speaker") } // Play starts playing all provided Streamers through the speaker. +// Each player will be closed automatically after the streamer ends. +// +// Play is a convenience function for (some) backwards compatibility. +// To have more control, use NewPlayer() instead. func Play(s ...beep.Streamer) { - mu.Lock() - mixer.Add(s...) - mu.Unlock() + for _, s := range s { + p := NewPlayer(s) + p.Play() + p.autoClosePlayer = true + } +} + +// NewPlayer creates a Player from a Streamer. Player can be used to finely control playback +// of the stream. A player has an internal buffer that will be filled by pulling samples from +// Streamer and will be drained by the audio being played. So their position won't necessarily +// match exactly. If you want to clear the underlying buffer for some reasons e.g., you want to +// seek the position of s, call the player's Reset() function. +// NewPlayer is concurrent safe, however s cannot be used by multiple players. +func NewPlayer(s beep.Streamer) Player { + r := newReaderFromStreamer(s) + p := context.NewPlayer(r) + return Player{ + //s: s, + r: r, + p: p, + } } -// Clear removes all currently playing Streamers from the speaker. -func Clear() { - mu.Lock() - mixer.Clear() - mu.Unlock() +// sampleReader is a wrapper for beep.Streamer to implement io.Reader. +type sampleReader struct { + s beep.Streamer + buf [][2]float64 + num int } -// update pulls new data from the playing Streamers and sends it to the speaker. Blocks until the -// data is sent and started playing. -func update() { - mu.Lock() - mixer.Stream(samples) - mu.Unlock() +func newReaderFromStreamer(s beep.Streamer) *sampleReader { + return &sampleReader{ + s: s, + } +} + +// Read pulls samples from the reader and fills buf with the encoded +// samples. Read expects the size of buf be divisible by the length +// of a sample (= channel count * bit depth in bytes). +func (s *sampleReader) Read(buf []byte) (n int, err error) { + // Read samples from streamer + if len(buf)%bytesPerSample != 0 { + return 0, errors.New("requested number of bytes do not align with the samples") + } + ns := len(buf) / bytesPerSample + if len(s.buf) < ns { + s.buf = make([][2]float64, ns) + } + ns, ok := s.s.Stream(s.buf[:ns]) + if !ok { + if s.s.Err() != nil { + return 0, errors.Wrap(s.s.Err(), "streamer returned error when requesting samples") + } + if ns == 0 { + // TODO: close player if autoClosePlayer is set but only after all samples have been played. + return 0, io.EOF + } + } + s.num += ns - for i := range samples { - for c := range samples[i] { - val := samples[i][c] + // Convert samples to bytes + for i := range s.buf[:ns] { + for c := range s.buf[i] { + val := s.buf[i][c] if val < -1 { val = -1 } @@ -121,10 +132,84 @@ func update() { valInt16 := int16(val * (1<<15 - 1)) low := byte(valInt16) high := byte(valInt16 >> 8) - buf[i*4+c*2+0] = low - buf[i*4+c*2+1] = high + buf[i*bytesPerSample+c*bitDepthInBytes+0] = low + buf[i*bytesPerSample+c*bitDepthInBytes+1] = high } } - player.Write(buf) + return ns * bytesPerSample, nil +} + +// Player gives control over the playback of the Steamer. +type Player struct { + //s beep.Streamer + r *sampleReader + p oto.Player + autoClosePlayer bool +} + +// Pause the player. +func (s *Player) Pause() { + s.p.Pause() +} + +// Play resumes playing after playback was paused. +func (s *Player) Play() { + s.p.Play() +} + +// IsPlaying reports whether this player is playing. +func (s *Player) IsPlaying() bool { + return s.p.IsPlaying() +} + +// Reset clears the underlying buffer and pauses its playing. +// This can be useful when you want to Seek() or make other +// modifications to the Streamer. In this case, call Reset() +// before seeking or other functions. +// Reset will also reset SamplesPlayed and DurationPlayed. +func (s *Player) Reset() { + s.p.Reset() + s.r.num = 0 +} + +// Volume returns the current volume in the range of [0, 1]. +// The default volume is 1. +// TODO: allow changing/reading volume in other bases. See Beep's Volume effect. +func (s *Player) Volume() float64 { + return s.p.Volume() +} + +// SetVolume sets the current volume in the range of [0, 1]. +func (s *Player) SetVolume(v float64) { + s.p.SetVolume(v) +} + +// UnplayedBufferSize returns the byte size in the underlying buffer that is not played yet. +func (s *Player) UnplayedBufferSize() int { + return s.p.UnplayedBufferSize() +} + +// SamplesPlayed returns the total number of samples played through this player. +// If the player is in a paused state, this will not increase. +func (s *Player) SamplesPlayed() int { + return s.r.num - s.p.UnplayedBufferSize() / bytesPerSample +} + +// DurationPlayed returns the playtime of this player based on the samples played and +// the sample rate speaker was initialized with. +// If the player is in a paused state, this will not increase. +func (s *Player) DurationPlayed() time.Duration { + return contextSampleRate.D(s.SamplesPlayed()) +} + +// Err returns an error if this player has an error. +func (s *Player) Err() error { + return s.p.Err() +} + +// Close the player and remove it from the speaker. +// Streamer is *not* closed. +func (s *Player) Close() error { + return s.p.Close() }