diff --git a/audio/audio.go b/audio/audio.go index dd588cf1b..1af03c037 100644 --- a/audio/audio.go +++ b/audio/audio.go @@ -40,8 +40,6 @@ import ( "sync" "time" - "github.com/hajimehoshi/oto" - "github.com/hajimehoshi/ebiten/internal/hooks" "github.com/hajimehoshi/ebiten/internal/web" ) @@ -53,6 +51,9 @@ import ( // // For a typical usage example, see examples/wav/main.go. type Context struct { + c context + initCh chan struct{} + mux *mux sampleRate int err error @@ -84,8 +85,17 @@ func NewContext(sampleRate int) (*Context, error) { if theContext != nil { panic("audio: context is already created") } + + ch := make(chan struct{}) + + context, err := newContext(sampleRate, ch) + if err != nil { + return nil, err + } c := &Context{ sampleRate: sampleRate, + c: context, + initCh: ch, } theContext = c c.mux = newMux() @@ -103,36 +113,6 @@ func CurrentContext() *Context { return c } -type context interface { - NewPlayer() io.WriteCloser - io.Closer -} - -var contextForTesting context - -type otoContext struct { - c *oto.Context -} - -func (d *otoContext) NewPlayer() io.WriteCloser { - return d.c.NewPlayer() -} - -func (d *otoContext) Close() error { - return d.c.Close() -} - -func newContext(sampleRate int) (context, error) { - if contextForTesting != nil { - return contextForTesting, nil - } - c, err := oto.NewContext(sampleRate, channelNum, bytesPerSample/channelNum, bufferSize()) - if err != nil { - return nil, err - } - return &otoContext{c}, nil -} - func (c *Context) loop() { suspendCh := make(chan struct{}, 1) resumeCh := make(chan struct{}, 1) @@ -143,11 +123,10 @@ func (c *Context) loop() { resumeCh <- struct{}{} }) - initCh := make(chan struct{}) var once sync.Once hooks.AppendHookOnBeforeUpdate(func() error { once.Do(func() { - close(initCh) + close(c.initCh) }) var err error @@ -161,21 +140,11 @@ func (c *Context) loop() { return err }) - // Initialize oto.Player lazily to enable calling NewContext in an 'init' function. - // Accessing oto.Player functions requires the environment to be already initialized, - // 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. - <-initCh + <-c.initCh - context, err := newContext(c.sampleRate) - if err != nil { - c.err = err - return - } - defer context.Close() + defer c.c.Close() - p := context.NewPlayer() + p := c.c.NewPlayer() defer p.Close() for { diff --git a/audio/context.go b/audio/context.go new file mode 100644 index 000000000..3719bb0fe --- /dev/null +++ b/audio/context.go @@ -0,0 +1,102 @@ +// Copyright 2019 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 audio + +import ( + "io" + "sync" + + "github.com/hajimehoshi/oto" +) + +type context interface { + NewPlayer() io.WriteCloser + io.Closer +} + +var contextForTesting context + +type otoContext struct { + sampleRate int + initCh <-chan struct{} + + c *oto.Context + once sync.Once +} + +func (c *otoContext) NewPlayer() io.WriteCloser { + return &otoPlayer{c: c} +} + +func (c *otoContext) Close() error { + if c.c == nil { + return nil + } + return c.c.Close() +} + +func (c *otoContext) ensureContext() error { + var err error + c.once.Do(func() { + <-c.initCh + c.c, err = oto.NewContext(c.sampleRate, channelNum, bytesPerSample/channelNum, bufferSize()) + }) + return err +} + +type otoPlayer struct { + c *otoContext + p *oto.Player + once sync.Once +} + +func (p *otoPlayer) Write(buf []byte) (int, error) { + // Initialize oto.Player lazily to enable calling NewContext in an 'init' function. + // Accessing oto.Player functions requires the environment to be already initialized, + // 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 err := p.ensurePlayer(); err != nil { + return 0, err + } + return p.p.Write(buf) +} + +func (p *otoPlayer) Close() error { + if p.p == nil { + return nil + } + return p.p.Close() +} + +func (p *otoPlayer) ensurePlayer() error { + if err := p.c.ensureContext(); err != nil { + return err + } + p.once.Do(func() { + p.p = p.c.c.NewPlayer() + }) + return nil +} + +func newContext(sampleRate int, initCh <-chan struct{}) (context, error) { + if contextForTesting != nil { + return contextForTesting, nil + } + return &otoContext{ + sampleRate: sampleRate, + initCh: initCh, + }, nil +}