From b46cb324ed6924f97ff508e57506bc6a634ecc84 Mon Sep 17 00:00:00 2001 From: Hajime Hoshi Date: Sun, 28 Mar 2021 18:07:38 +0900 Subject: [PATCH] audio: Add audio/internal/readerdriver package --- audio/audio.go | 3 +- audio/export_test.go | 26 +++-- audio/internal/go2cpp/player_js.go | 28 ++--- audio/internal/readerdriver/driver.go | 36 ++++++ .../readerdriver/driver_default.go} | 8 +- .../readerdriver/driver_js.go} | 109 +++++++++++------- audio/readerplayer.go | 103 +++++++---------- 7 files changed, 179 insertions(+), 134 deletions(-) create mode 100644 audio/internal/readerdriver/driver.go rename audio/{readerplayer_default.go => internal/readerdriver/driver_default.go} (73%) rename audio/{readerplayer_js.go => internal/readerdriver/driver_js.go} (67%) diff --git a/audio/audio.go b/audio/audio.go index b2898a9f1..e13339ae2 100644 --- a/audio/audio.go +++ b/audio/audio.go @@ -40,6 +40,7 @@ import ( "sync" "time" + "github.com/hajimehoshi/ebiten/v2/audio/internal/readerdriver" "github.com/hajimehoshi/ebiten/v2/internal/hooks" ) @@ -101,7 +102,7 @@ func NewContext(sampleRate int) *Context { } var np newPlayerImpler - if isReaderContextAvailable() { + if readerdriver.IsAvailable() { // 'Reader players' are players that implement io.Reader. This is the new way and // not all the environments support reader players. Reader players can have enough // buffers so that clicking noises can be avoided compared to writer players. diff --git a/audio/export_test.go b/audio/export_test.go index ccb148d29..8a4eb7c83 100644 --- a/audio/export_test.go +++ b/audio/export_test.go @@ -18,18 +18,20 @@ import ( "io" "io/ioutil" "sync" + + "github.com/hajimehoshi/ebiten/v2/audio/internal/readerdriver" ) type ( - dummyWriterPlayerDriver struct{} - dummyWriterPlayer struct{} + dummyWriterContext struct{} + dummyWriterPlayer struct{} ) -func (d *dummyWriterPlayerDriver) NewPlayer() io.WriteCloser { +func (c *dummyWriterContext) NewPlayer() io.WriteCloser { return &dummyWriterPlayer{} } -func (d *dummyWriterPlayerDriver) Close() error { +func (c *dummyWriterContext) Close() error { return nil } @@ -42,12 +44,12 @@ func (p *dummyWriterPlayer) Close() error { } func init() { - writerDriverForTesting = &dummyWriterPlayerDriver{} + writerDriverForTesting = &dummyWriterContext{} } type ( - dummyReaderPlayerDriver struct{} - dummyReaderPlayer struct { + dummyReaderContext struct{} + dummyReaderPlayer struct { r io.Reader playing bool volume float64 @@ -55,14 +57,18 @@ type ( } ) -func (d *dummyReaderPlayerDriver) NewPlayer(r io.Reader) readerDriverPlayer { +func (c *dummyReaderContext) NewPlayer(r io.Reader) readerdriver.Player { return &dummyReaderPlayer{ r: r, volume: 1, } } -func (d *dummyReaderPlayerDriver) Close() error { +func (c *dummyReaderContext) MaxBufferSize() int { + return 48000 * channelNum * bitDepthInBytes / 4 +} + +func (c *dummyReaderContext) Close() error { return nil } @@ -118,7 +124,7 @@ func (p *dummyReaderPlayer) Close() error { } func init() { - readerDriverForTesting = &dummyReaderPlayerDriver{} + readerDriverForTesting = &dummyReaderContext{} } type dummyHook struct { diff --git a/audio/internal/go2cpp/player_js.go b/audio/internal/go2cpp/player_js.go index 6f38c0abf..7a334b4a0 100644 --- a/audio/internal/go2cpp/player_js.go +++ b/audio/internal/go2cpp/player_js.go @@ -63,6 +63,16 @@ func (c *Context) Close() error { return nil } +func (c *Context) oneBufferSize() int { + // TODO: This must be audio.oneBufferSize(p.context.sampleRate). Avoid the duplication. + return c.sampleRate * c.channelNum * c.bitDepthInBytes / 4 +} + +func (c *Context) MaxBufferSize() int { + // TODO: This must be audio.maxBufferSize(p.context.sampleRate). Avoid the duplication. + return c.oneBufferSize() * 2 +} + type playerState int const ( @@ -100,16 +110,6 @@ func (p *Player) Pause() { p.cond.Signal() } -func (p *Player) oneBufferSize() int { - // TODO: This must be audio.oneBufferSize(p.context.sampleRate). Avoid the duplication. - return p.context.sampleRate * p.context.channelNum * p.context.bitDepthInBytes / 4 -} - -func (p *Player) maxBufferSize() int { - // TODO: This must be audio.maxBufferSize(p.context.sampleRate). Avoid the duplication. - return p.oneBufferSize() * 2 -} - func (p *Player) Play() { p.cond.L.Lock() defer p.cond.L.Unlock() @@ -130,8 +130,8 @@ func (p *Player) Play() { // Prepare the first data as soon as possible, or the audio can get stuck. // TODO: Get the appropriate buffer size from the C++ side. if p.buf == nil { - n := p.oneBufferSize() - if max := p.maxBufferSize() - int(p.UnplayedBufferSize()); n > max { + n := p.context.oneBufferSize() + if max := p.context.MaxBufferSize() - int(p.UnplayedBufferSize()); n > max { n = max } p.buf = make([]byte, n) @@ -248,7 +248,7 @@ func (p *Player) shouldWait() bool { case playerStatePaused: return true case playerStatePlaying: - return p.v.Get("unplayedBufferSize").Int() >= p.maxBufferSize() + return p.v.Get("unplayedBufferSize").Int() >= p.context.MaxBufferSize() } return false } @@ -291,7 +291,7 @@ func (p *Player) loop() { } n := readChunkSize - if max := p.maxBufferSize() - int(p.UnplayedBufferSize()); n > max { + if max := p.context.MaxBufferSize() - int(p.UnplayedBufferSize()); n > max { n = max } n2, err := p.src.Read(buf[:n]) diff --git a/audio/internal/readerdriver/driver.go b/audio/internal/readerdriver/driver.go new file mode 100644 index 000000000..e49c7a0fc --- /dev/null +++ b/audio/internal/readerdriver/driver.go @@ -0,0 +1,36 @@ +// Copyright 2021 The Ebiten Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package readerdriver + +import ( + "io" +) + +type Context interface { + NewPlayer(io.Reader) Player + MaxBufferSize() int + io.Closer +} + +type Player interface { + Pause() + Play() + IsPlaying() bool + Reset() + Volume() float64 + SetVolume(volume float64) + UnplayedBufferSize() int64 + io.Closer +} diff --git a/audio/readerplayer_default.go b/audio/internal/readerdriver/driver_default.go similarity index 73% rename from audio/readerplayer_default.go rename to audio/internal/readerdriver/driver_default.go index d21b8f4e0..2e583e2b4 100644 --- a/audio/readerplayer_default.go +++ b/audio/internal/readerdriver/driver_default.go @@ -14,17 +14,17 @@ // +build !js -package audio +package readerdriver import ( "fmt" "runtime" ) -func isReaderContextAvailable() bool { +func IsAvailable() bool { return false } -func newReaderDriverImpl(context *Context) (readerDriver, error) { - panic(fmt.Sprintf("audio: newReaderDriver is not available on this environment: GOOS=%s", runtime.GOOS)) +func NewContext(sampleRate int, channelNum int, bitDepthInBytes int) (Context, error) { + panic(fmt.Sprintf("readerdriver: NewContext is not available on this environment: GOOS=%s", runtime.GOOS)) } diff --git a/audio/readerplayer_js.go b/audio/internal/readerdriver/driver_js.go similarity index 67% rename from audio/readerplayer_js.go rename to audio/internal/readerdriver/driver_js.go index 594730690..833242067 100644 --- a/audio/readerplayer_js.go +++ b/audio/internal/readerdriver/driver_js.go @@ -12,7 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -package audio +package readerdriver import ( "errors" @@ -26,20 +26,23 @@ import ( "github.com/hajimehoshi/ebiten/v2/internal/jsutil" ) -func isReaderContextAvailable() bool { +func IsAvailable() bool { return true } -type readerDriverImpl struct { - context *Context +type contextImpl struct { audioContext js.Value ready bool callbacks map[string]js.Func + + sampleRate int + channelNum int + bitDepthInBytes int } -func newReaderDriverImpl(context *Context) (readerDriver, error) { +func NewContext(sampleRate int, channelNum int, bitDepthInBytes int) (Context, error) { if js.Global().Get("go2cpp").Truthy() { - return &go2cppDriverWrapper{go2cpp.NewContext(context.sampleRate, channelNum, bitDepthInBytes)}, nil + return &go2cppDriverWrapper{go2cpp.NewContext(sampleRate, channelNum, bitDepthInBytes)}, nil } class := js.Global().Get("AudioContext") @@ -47,14 +50,16 @@ func newReaderDriverImpl(context *Context) (readerDriver, error) { class = js.Global().Get("webkitAudioContext") } if !class.Truthy() { - return nil, errors.New("audio: AudioContext or webkitAudioContext was not found") + return nil, errors.New("readerdriver: AudioContext or webkitAudioContext was not found") } options := js.Global().Get("Object").New() - options.Set("sampleRate", context.sampleRate) + options.Set("sampleRate", sampleRate) - d := &readerDriverImpl{ - context: context, - audioContext: class.New(options), + d := &contextImpl{ + audioContext: class.New(options), + sampleRate: sampleRate, + channelNum: channelNum, + bitDepthInBytes: bitDepthInBytes, } setCallback := func(event string) js.Func { @@ -90,36 +95,51 @@ const ( readerPlayerClosed ) -type readerDriverPlayerImpl struct { - driver *readerDriverImpl - src io.Reader - eof bool - state readerPlayerState - gain js.Value +type playerImpl struct { + context *contextImpl + src io.Reader + eof bool + state readerPlayerState + gain js.Value + err error nextPos float64 bufferSourceNodes []js.Value appendBufferFunc js.Func } -func (d *readerDriverImpl) NewPlayer(src io.Reader) readerDriverPlayer { - p := &readerDriverPlayerImpl{ - driver: d, - src: src, - gain: d.audioContext.Call("createGain"), +func (c *contextImpl) NewPlayer(src io.Reader) Player { + p := &playerImpl{ + context: c, + src: src, + gain: c.audioContext.Call("createGain"), } p.appendBufferFunc = js.FuncOf(p.appendBuffer) - p.gain.Call("connect", d.audioContext.Get("destination")) - runtime.SetFinalizer(p, (*readerDriverPlayerImpl).Close) + p.gain.Call("connect", c.audioContext.Get("destination")) + runtime.SetFinalizer(p, (*playerImpl).Close) return p } -func (d *readerDriverImpl) Close() error { +func (c *contextImpl) Close() error { // TODO: Implement this return nil } -func (p *readerDriverPlayerImpl) Pause() { +// TODO: The term 'buffer' is confusing. Name each buffer with good terms. + +// oneBufferSize returns the size of one buffer in the player implementation. +func (c *contextImpl) oneBufferSize() int { + return c.sampleRate * c.channelNum * c.bitDepthInBytes / 4 +} + +// maxBufferSize returns the maximum size of the buffer for the audio source. +// This buffer is used when unreading on pausing the player. +func (c *contextImpl) MaxBufferSize() int { + // The number of underlying buffers should be 2. + return c.oneBufferSize() * 2 +} + +func (p *playerImpl) Pause() { if p.state != readerPlayerPlay { return } @@ -135,7 +155,7 @@ func (p *readerDriverPlayerImpl) Pause() { p.nextPos = 0 } -func (p *readerDriverPlayerImpl) appendBuffer(this js.Value, args []js.Value) interface{} { +func (p *playerImpl) appendBuffer(this js.Value, args []js.Value) interface{} { // appendBuffer is called as the 'ended' callback of a buffer. // 'this' is an AudioBufferSourceNode that already finishes its playing. for i, n := range p.bufferSourceNodes { @@ -156,17 +176,18 @@ func (p *readerDriverPlayerImpl) appendBuffer(this js.Value, args []js.Value) in return nil } - c := p.driver.audioContext.Get("currentTime").Float() + c := p.context.audioContext.Get("currentTime").Float() if p.nextPos < c { // The exact current time might be too early. Add some delay on purpose to avoid buffer overlapping. p.nextPos = c + 1.0/60.0 } - bs := make([]byte, oneBufferSize(p.driver.context.sampleRate)) + bs := make([]byte, p.context.oneBufferSize()) n, err := io.ReadFull(p.src, bs) if err != nil { if err != io.EOF && err != io.ErrUnexpectedEOF { - p.driver.context.setError(err) + p.err = err + p.Pause() return nil } p.eof = true @@ -178,7 +199,7 @@ func (p *readerDriverPlayerImpl) appendBuffer(this js.Value, args []js.Value) in l, r := toLR(bs) tl, tr := float32SliceToTypedArray(l), float32SliceToTypedArray(r) - buf := p.driver.audioContext.Call("createBuffer", channelNum, len(bs)/channelNum/bitDepthInBytes, p.driver.context.sampleRate) + buf := p.context.audioContext.Call("createBuffer", p.context.channelNum, len(bs)/p.context.channelNum/p.context.bitDepthInBytes, p.context.sampleRate) if buf.Get("copyToChannel").Truthy() { buf.Call("copyToChannel", tl, 0, 0) buf.Call("copyToChannel", tr, 1, 0) @@ -188,7 +209,7 @@ func (p *readerDriverPlayerImpl) appendBuffer(this js.Value, args []js.Value) in buf.Call("getChannelData", 1).Call("set", tr) } - s := p.driver.audioContext.Call("createBufferSource") + s := p.context.audioContext.Call("createBufferSource") s.Set("buffer", buf) s.Set("onended", p.appendBufferFunc) s.Call("connect", p.gain) @@ -199,7 +220,7 @@ func (p *readerDriverPlayerImpl) appendBuffer(this js.Value, args []js.Value) in return nil } -func (p *readerDriverPlayerImpl) Play() { +func (p *playerImpl) Play() { if p.state != readerPlayerPaused { return } @@ -208,11 +229,11 @@ func (p *readerDriverPlayerImpl) Play() { p.appendBuffer(js.Undefined(), nil) } -func (p *readerDriverPlayerImpl) IsPlaying() bool { +func (p *playerImpl) IsPlaying() bool { return p.state == readerPlayerPlay } -func (p *readerDriverPlayerImpl) Reset() { +func (p *playerImpl) Reset() { if p.state == readerPlayerClosed { return } @@ -221,39 +242,43 @@ func (p *readerDriverPlayerImpl) Reset() { p.eof = false } -func (p *readerDriverPlayerImpl) Volume() float64 { +func (p *playerImpl) Volume() float64 { return p.gain.Get("gain").Get("value").Float() } -func (p *readerDriverPlayerImpl) SetVolume(volume float64) { +func (p *playerImpl) SetVolume(volume float64) { p.gain.Get("gain").Set("value", volume) } -func (p *readerDriverPlayerImpl) UnplayedBufferSize() int64 { +func (p *playerImpl) UnplayedBufferSize() int64 { // This is not an accurate buffer size as part of the buffers might already be consumed. var sec float64 for _, n := range p.bufferSourceNodes { sec += n.Get("buffer").Get("duration").Float() } - return int64(sec * float64(p.driver.context.sampleRate*channelNum*bitDepthInBytes)) + return int64(sec * float64(p.context.sampleRate*p.context.channelNum*p.context.bitDepthInBytes)) } -func (p *readerDriverPlayerImpl) Close() error { +func (p *playerImpl) Close() error { runtime.SetFinalizer(p, nil) p.Reset() p.state = readerPlayerClosed p.appendBufferFunc.Release() - return nil + return p.err } type go2cppDriverWrapper struct { c *go2cpp.Context } -func (w *go2cppDriverWrapper) NewPlayer(r io.Reader) readerDriverPlayer { +func (w *go2cppDriverWrapper) NewPlayer(r io.Reader) Player { return w.c.NewPlayer(r) } +func (w *go2cppDriverWrapper) MaxBufferSize() int { + return w.c.MaxBufferSize() +} + func (w *go2cppDriverWrapper) Close() error { return w.c.Close() } diff --git a/audio/readerplayer.go b/audio/readerplayer.go index 674a5c49a..e1b2d9871 100644 --- a/audio/readerplayer.go +++ b/audio/readerplayer.go @@ -20,52 +20,23 @@ import ( "runtime" "sync" "time" + + "github.com/hajimehoshi/ebiten/v2/audio/internal/readerdriver" ) -// TODO: The term 'buffer' is confusing. Name each buffer with good terms. - -// oneBufferSize returns the size of one buffer in the player implementation. -func oneBufferSize(sampleRate int) int { - return sampleRate * channelNum * bitDepthInBytes / 4 -} - -// maxBufferSize returns the maximum size of the buffer for the audio source. -// This buffer is used when unreading on pausing the player. -func maxBufferSize(sampleRate int) int { - // The number of underlying buffers should be 2. - return oneBufferSize(sampleRate) * 2 -} - -// readerDriver represents a driver using io.ReadClosers. -type readerDriver interface { - NewPlayer(io.Reader) readerDriverPlayer - io.Closer -} - -type readerDriverPlayer interface { - Pause() - Play() - IsPlaying() bool - Reset() - Volume() float64 - SetVolume(volume float64) - UnplayedBufferSize() int64 - io.Closer -} - type readerPlayerFactory struct { - driver readerDriver + context readerdriver.Context sampleRate int } -var readerDriverForTesting readerDriver +var readerDriverForTesting readerdriver.Context func newReaderPlayerFactory(sampleRate int) *readerPlayerFactory { f := &readerPlayerFactory{ sampleRate: sampleRate, } if readerDriverForTesting != nil { - f.driver = readerDriverForTesting + f.context = readerDriverForTesting } // TODO: Consider the hooks. return f @@ -73,22 +44,17 @@ func newReaderPlayerFactory(sampleRate int) *readerPlayerFactory { type readerPlayer struct { context *Context - player readerDriverPlayer + player readerdriver.Player + src io.Reader stream *timeStream factory *readerPlayerFactory m sync.Mutex } func (f *readerPlayerFactory) newPlayerImpl(context *Context, src io.Reader) (playerImpl, error) { - sampleRate := context.SampleRate() - s, err := newTimeStream(src, sampleRate) - if err != nil { - return nil, err - } - p := &readerPlayer{ + src: src, context: context, - stream: s, factory: f, } runtime.SetFinalizer(p, (*readerPlayer).Close) @@ -101,15 +67,22 @@ func (p *readerPlayer) ensurePlayer() error { // but if Ebiten is used for a shared library, the timing when init functions are called // is unexpectable. // e.g. a variable for JVM on Android might not be set. - if p.factory.driver == nil { - d, err := newReaderDriverImpl(p.context) + if p.factory.context == nil { + c, err := readerdriver.NewContext(p.factory.sampleRate, channelNum, bitDepthInBytes) if err != nil { return err } - p.factory.driver = d + p.factory.context = c + } + if p.stream == nil { + s, err := newTimeStream(p.src, p.factory.sampleRate, p.factory.context.MaxBufferSize()) + if err != nil { + return err + } + p.stream = s } if p.player == nil { - p.player = p.factory.driver.NewPlayer(p.stream) + p.player = p.factory.context.NewPlayer(p.stream) } return nil } @@ -213,33 +186,37 @@ func (p *readerPlayer) Seek(offset time.Duration) error { p.m.Lock() defer p.m.Unlock() - if p.player != nil { - if p.player.IsPlaying() { - defer func() { - p.player.Play() - }() - } - p.player.Reset() + if err := p.ensurePlayer(); err != nil { + return err } + + if p.player.IsPlaying() { + defer func() { + p.player.Play() + }() + } + p.player.Reset() return p.stream.Seek(offset) } func (p *readerPlayer) source() io.Reader { - return p.stream.r + return p.src } type timeStream struct { - r io.Reader - sampleRate int - pos int64 - buf []byte - unread int + r io.Reader + sampleRate int + pos int64 + buf []byte + unread int + maxBufferSize int } -func newTimeStream(r io.Reader, sampleRate int) (*timeStream, error) { +func newTimeStream(r io.Reader, sampleRate int, maxBufferSize int) (*timeStream, error) { s := &timeStream{ - r: r, - sampleRate: sampleRate, + r: r, + sampleRate: sampleRate, + maxBufferSize: maxBufferSize, } if seeker, ok := s.r.(io.Seeker); ok { // Get the current position of the source. @@ -271,7 +248,7 @@ func (s *timeStream) Read(buf []byte) (int, error) { n, err := s.r.Read(buf) s.pos += int64(n) s.buf = append(s.buf, buf[:n]...) - if m := maxBufferSize(s.sampleRate); len(s.buf) > m { + if m := s.maxBufferSize; len(s.buf) > m { s.buf = s.buf[len(s.buf)-m:] } return n, err