-
Notifications
You must be signed in to change notification settings - Fork 151
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Use Oto v2 #130
base: master
Are you sure you want to change the base?
Use Oto v2 #130
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -2,21 +2,22 @@ | |
package speaker | ||
|
||
import ( | ||
"sync" | ||
|
||
"github.com/faiface/beep" | ||
"github.com/hajimehoshi/oto" | ||
"github.com/hajimehoshi/oto/v2" | ||
"github.com/pkg/errors" | ||
"io" | ||
"sync" | ||
) | ||
|
||
const channelCount = 2 | ||
const bitDepthInBytes = 2 | ||
const bytesPerSample = bitDepthInBytes * channelCount | ||
|
||
var ( | ||
mu sync.Mutex | ||
mixer beep.Mixer | ||
samples [][2]float64 | ||
buf []byte | ||
context *oto.Context | ||
player *oto.Player | ||
done chan struct{} | ||
player oto.Player | ||
) | ||
|
||
// Init initializes audio playback through speaker. Must be called before using this package. | ||
|
@@ -25,57 +26,40 @@ var ( | |
// 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() | ||
if context != nil { | ||
return errors.New("speaker cannot be initialized more than once") | ||
} | ||
|
||
mixer = beep.Mixer{} | ||
|
||
numBytes := bufferSize * 4 | ||
samples = make([][2]float64, bufferSize) | ||
buf = make([]byte, numBytes) | ||
|
||
var err error | ||
context, err = oto.NewContext(int(sampleRate), 2, 2, numBytes) | ||
var readyChan chan struct{} | ||
context, readyChan, err = oto.NewContext(int(sampleRate), channelCount, bitDepthInBytes) | ||
if err != nil { | ||
return errors.Wrap(err, "failed to initialize speaker") | ||
} | ||
player = context.NewPlayer() | ||
|
||
done = make(chan struct{}) | ||
<-readyChan | ||
|
||
go func() { | ||
for { | ||
select { | ||
default: | ||
update() | ||
case <-done: | ||
return | ||
} | ||
} | ||
}() | ||
player = context.NewPlayer(newReaderFromStreamer(&mixer)) | ||
player.(oto.BufferSizeSetter).SetBufferSize(bufferSize * bytesPerSample) | ||
player.Play() | ||
|
||
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. | ||
// Close closes audio playback. However, the underlying driver context keeps existing, because | ||
// closing it isn't supported (https://github.com/hajimehoshi/oto/issues/149). 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. | ||
func Close() { | ||
if player != nil { | ||
if done != nil { | ||
done <- struct{}{} | ||
done = nil | ||
} | ||
player.Close() | ||
context.Close() | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. So closing the context isn't supported by Oto. I think this is the only breaking change. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yeah, is this problematic? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I mentioned it because I don't really understand what happens when the context is or isn't closed. If it wouldn't be problematic with Oto, I don't think it will be with Beep. However, I do not know if the method description of |
||
player = nil | ||
Clear() | ||
} | ||
} | ||
|
||
// Lock locks the speaker. While locked, speaker won't pull new data from the playing Stramers. Lock | ||
// Lock locks the speaker. While locked, speaker won't pull new data from the playing Streamers. Lock | ||
// if you want to modify any currently playing Streamers to avoid race conditions. | ||
// | ||
// Always lock speaker for as little time as possible, to avoid playback glitches. | ||
|
@@ -96,22 +80,51 @@ func Play(s ...beep.Streamer) { | |
} | ||
|
||
// Clear removes all currently playing Streamers from the speaker. | ||
// Previously buffered samples may still be played. | ||
func Clear() { | ||
mu.Lock() | ||
mixer.Clear() | ||
mu.Unlock() | ||
} | ||
|
||
// 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) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I see that in the previous version, error's are ignored. Is this intentional? |
||
mu.Unlock() | ||
// sampleReader is a wrapper for beep.Streamer to implement io.Reader. | ||
type sampleReader struct { | ||
s beep.Streamer | ||
buf [][2]float64 | ||
} | ||
|
||
func newReaderFromStreamer(s beep.Streamer) *sampleReader { | ||
return &sampleReader{ | ||
s: s, | ||
} | ||
} | ||
|
||
// Read pulls samples from the streamer 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.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 { | ||
return 0, io.EOF | ||
} | ||
} | ||
|
||
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 +134,18 @@ 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 | ||
} | ||
|
||
// stream pull samples from the streamer while preventing concurrency | ||
// problems by locking the global mixer. | ||
func (s *sampleReader) stream(samples [][2]float64) (n int, ok bool) { | ||
mu.Lock() | ||
defer mu.Unlock() | ||
return s.s.Stream(samples) | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The alpha version contains a fix for this locking problem
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
alpha.5 was released very recently.