diff --git a/audio/audio.go b/audio/audio.go index e31fa4d85..ba28c356d 100644 --- a/audio/audio.go +++ b/audio/audio.go @@ -40,7 +40,6 @@ import ( "sync" "time" - "github.com/hajimehoshi/ebiten/v2/audio/internal/readerdriver" "github.com/hajimehoshi/ebiten/v2/internal/hooks" ) @@ -50,10 +49,6 @@ const ( bytesPerSample = bitDepthInBytes * channelNum ) -type newPlayerImpler interface { - newPlayerImpl(context *Context, src io.Reader) (playerImpl, error) -} - // A Context represents a current state of audio. // // At most one Context object can exist in one process. @@ -61,7 +56,7 @@ type newPlayerImpler interface { // // For a typical usage example, see examples/wav/main.go. type Context struct { - np newPlayerImpler + np *readerPlayerFactory // inited represents whether the audio device is initialized and available or not. // On Android, audio loop cannot be started unless JVM is accessible. After updating one frame, JVM should exist. @@ -102,23 +97,9 @@ func NewContext(sampleRate int) *Context { panic("audio: context is already created") } - var np newPlayerImpler - 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. - // Reder players will replace writer players in any platforms in the future. - np = newReaderPlayerFactory(sampleRate) - } else { - // 'Writer players' are players that implement io.Writer. This is the old way but - // all the environments support writer players. Writer players cannot have enough - // buffers and clicking noises are sometimes problematic (#1356, #1458). - np = newWriterPlayerFactory(sampleRate) - } - c := &Context{ sampleRate: sampleRate, - np: np, + np: newReaderPlayerFactory(sampleRate), players: map[playerImpl]struct{}{}, inited: make(chan struct{}), semaphore: make(chan struct{}, 1), @@ -128,16 +109,12 @@ func NewContext(sampleRate int) *Context { h := getHook() h.OnSuspendAudio(func() error { c.semaphore <- struct{}{} - if s, ok := np.(interface{ suspend() error }); ok { - return s.suspend() - } + c.np.suspend() return nil }) h.OnResumeAudio(func() error { <-c.semaphore - if s, ok := np.(interface{ resume() error }); ok { - return s.resume() - } + c.np.resume() return nil }) diff --git a/audio/internal/readerdriver/driver_android.go b/audio/internal/readerdriver/driver_android.go index ec6028867..00a31fd85 100644 --- a/audio/internal/readerdriver/driver_android.go +++ b/audio/internal/readerdriver/driver_android.go @@ -18,10 +18,6 @@ import ( "github.com/hajimehoshi/ebiten/v2/audio/internal/oboe" ) -func IsAvailable() bool { - return true -} - type context struct { sampleRate int channelNum int diff --git a/audio/internal/readerdriver/driver_darwin.go b/audio/internal/readerdriver/driver_darwin.go index fa163e16f..7ac192a77 100644 --- a/audio/internal/readerdriver/driver_darwin.go +++ b/audio/internal/readerdriver/driver_darwin.go @@ -30,10 +30,6 @@ import ( "unsafe" ) -func IsAvailable() bool { - return true -} - const ( float32SizeInBytes = 4 ) diff --git a/audio/internal/readerdriver/driver_js.go b/audio/internal/readerdriver/driver_js.go index b974fdeb3..8918e7736 100644 --- a/audio/internal/readerdriver/driver_js.go +++ b/audio/internal/readerdriver/driver_js.go @@ -26,10 +26,6 @@ import ( "github.com/hajimehoshi/ebiten/v2/audio/internal/go2cpp" ) -func IsAvailable() bool { - return true -} - type context struct { audioContext js.Value ready bool diff --git a/audio/internal/readerdriver/driver_unix.go b/audio/internal/readerdriver/driver_unix.go index 9c4d1da71..73b0a3790 100644 --- a/audio/internal/readerdriver/driver_unix.go +++ b/audio/internal/readerdriver/driver_unix.go @@ -29,10 +29,6 @@ import ( "unsafe" ) -func IsAvailable() bool { - return true -} - type context struct { sampleRate int channelNum int @@ -84,6 +80,7 @@ func NewContext(sampleRate, channelNum, bitDepthInBytes int) (Context, chan stru buf32 := make([]float32, int(periodSize)*c.channelNum) for { if err := c.readAndWrite(buf32); err != nil { + // TODO: Handle errors correctly. panic(err) } } diff --git a/audio/internal/readerdriver/driver_windows.go b/audio/internal/readerdriver/driver_windows.go index f8ed1fce9..e4ca9ee69 100644 --- a/audio/internal/readerdriver/driver_windows.go +++ b/audio/internal/readerdriver/driver_windows.go @@ -26,10 +26,6 @@ import ( const headerBufferSize = 4096 -func IsAvailable() bool { - return true -} - type header struct { waveOut uintptr buffer []float32 diff --git a/audio/otodriver.go b/audio/otodriver.go deleted file mode 100644 index 91f9a4a63..000000000 --- a/audio/otodriver.go +++ /dev/null @@ -1,92 +0,0 @@ -// Copyright 2020 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" -) - -func newOtoDriver(sampleRate int, initCh chan struct{}) *otoDriver { - return &otoDriver{ - sampleRate: sampleRate, - initCh: initCh, - } -} - -type otoDriver struct { - sampleRate int - initCh <-chan struct{} - - c *oto.Context - once sync.Once -} - -func (c *otoDriver) NewPlayer() io.WriteCloser { - return &otoPlayer{c: c} -} - -func (c *otoDriver) Close() error { - if c.c == nil { - return nil - } - return c.c.Close() -} - -func (c *otoDriver) 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 *otoDriver - 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 -} diff --git a/audio/writerplayer.go b/audio/writerplayer.go deleted file mode 100644 index e9726fef1..000000000 --- a/audio/writerplayer.go +++ /dev/null @@ -1,290 +0,0 @@ -// 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 ( - "fmt" - "io" - "sync" - "time" - - "github.com/hajimehoshi/ebiten/v2/internal/hooks" -) - -// writerDriver represents a driver using io.WriteClosers. -// The actual implementation is otoDriver. -type writerDriver interface { - NewPlayer() io.WriteCloser - io.Closer -} - -var writerDriverForTesting writerDriver - -func newWriterDriver(sampleRate int) writerDriver { - if writerDriverForTesting != nil { - return writerDriverForTesting - } - - ch := make(chan struct{}) - var once sync.Once - hooks.AppendHookOnBeforeUpdate(func() error { - once.Do(func() { - close(ch) - }) - return nil - }) - return newOtoDriver(sampleRate, ch) -} - -type writerPlayerFactory struct { - driver writerDriver -} - -func newWriterPlayerFactory(sampleRate int) *writerPlayerFactory { - return &writerPlayerFactory{ - driver: newWriterDriver(sampleRate), - } -} - -type writerPlayer struct { - context *Context - driver writerDriver - src io.Reader - playing bool - closedExplicitly bool - isLoopActive bool - - buf []byte - readbuf []byte - pos int64 - volume float64 - - m sync.Mutex -} - -func (c *writerPlayerFactory) newPlayerImpl(context *Context, src io.Reader) (playerImpl, error) { - p := &writerPlayer{ - context: context, - driver: c.driver, - src: src, - volume: 1, - } - if seeker, ok := p.src.(io.Seeker); ok { - // Get the current position of the source. - pos, err := seeker.Seek(0, io.SeekCurrent) - if err != nil { - return nil, err - } - p.pos = pos - } - return p, nil -} - -func (p *writerPlayer) Close() error { - p.m.Lock() - defer p.m.Unlock() - - p.playing = false - if p.closedExplicitly { - return fmt.Errorf("audio: the player is already closed") - } - p.closedExplicitly = true - return nil -} - -func (p *writerPlayer) Play() { - p.m.Lock() - defer p.m.Unlock() - - if p.closedExplicitly { - p.context.setError(fmt.Errorf("audio: the player is already closed")) - return - } - - p.playing = true - if p.isLoopActive { - return - } - - // Set p.isLoopActive to true here, not in the loop. This prevents duplicated active loops. - p.isLoopActive = true - p.context.addPlayer(p) - - go p.loop() - return -} - -func (p *writerPlayer) loop() { - p.context.waitUntilInited() - - w := p.driver.NewPlayer() - wclosed := make(chan struct{}) - defer func() { - <-wclosed - w.Close() - }() - - defer func() { - p.m.Lock() - p.playing = false - p.context.removePlayer(p) - p.isLoopActive = false - p.m.Unlock() - }() - - ch := make(chan []byte) - defer close(ch) - - go func() { - for buf := range ch { - if _, err := w.Write(buf); err != nil { - p.context.setError(err) - break - } - p.context.setReady() - } - close(wclosed) - }() - - for { - buf, ok := p.read() - if !ok { - return - } - ch <- buf - } -} - -func (p *writerPlayer) read() ([]byte, bool) { - if p.context.hasError() { - return nil, false - } - - if p.closedExplicitly { - return nil, false - } - - p.context.acquireSemaphore() - defer func() { - p.context.releaseSemaphore() - }() - - p.m.Lock() - defer p.m.Unlock() - - // playing can be false when pausing. - if !p.playing { - return nil, false - } - - const bufSize = 2048 - if p.readbuf == nil { - p.readbuf = make([]byte, bufSize) - } - n, err := p.src.Read(p.readbuf[:bufSize-len(p.buf)]) - if err != nil { - if err != io.EOF { - p.context.setError(err) - return nil, false - } - if n == 0 { - return nil, false - } - } - buf := append(p.buf, p.readbuf[:n]...) - - n2 := len(buf) - len(buf)%bytesPerSample - buf, p.buf = buf[:n2], buf[n2:] - - for i := 0; i < len(buf)/2; i++ { - v16 := int16(buf[2*i]) | (int16(buf[2*i+1]) << 8) - v16 = int16(float64(v16) * p.volume) - buf[2*i] = byte(v16) - buf[2*i+1] = byte(v16 >> 8) - } - p.pos += int64(len(buf)) - - return buf, true -} - -func (p *writerPlayer) IsPlaying() bool { - p.m.Lock() - r := p.playing - p.m.Unlock() - return r -} - -func (p *writerPlayer) Rewind() error { - if _, ok := p.src.(io.Seeker); !ok { - panic("audio: player to be rewound must be io.Seeker") - } - return p.Seek(0) -} - -func (p *writerPlayer) Seek(offset time.Duration) error { - p.m.Lock() - defer p.m.Unlock() - - o := int64(offset) * bytesPerSample * int64(p.context.SampleRate()) / int64(time.Second) - o = o - (o % bytesPerSample) - - seeker, ok := p.src.(io.Seeker) - if !ok { - panic("audio: the source must be io.Seeker when seeking") - } - pos, err := seeker.Seek(o, io.SeekStart) - if err != nil { - return err - } - - p.buf = nil - p.pos = pos - return nil -} - -func (p *writerPlayer) Pause() { - p.m.Lock() - p.playing = false - p.m.Unlock() -} - -func (p *writerPlayer) Current() time.Duration { - p.m.Lock() - sample := p.pos / bytesPerSample - p.m.Unlock() - return time.Duration(sample) * time.Second / time.Duration(p.context.SampleRate()) -} - -func (p *writerPlayer) Volume() float64 { - p.m.Lock() - v := p.volume - p.m.Unlock() - return v -} - -func (p *writerPlayer) SetVolume(volume float64) { - // The condition must be true when volume is NaN. - if !(0 <= volume && volume <= 1) { - panic("audio: volume must be in between 0 and 1") - } - - p.m.Lock() - p.volume = volume - p.m.Unlock() -} - -func (p *writerPlayer) source() io.Reader { - return p.src -} diff --git a/go.mod b/go.mod index ffc5d69b8..4c59a6da0 100644 --- a/go.mod +++ b/go.mod @@ -8,7 +8,6 @@ require ( github.com/hajimehoshi/bitmapfont/v2 v2.1.3 github.com/hajimehoshi/file2byteslice v0.0.0-20200812174855-0e5e8a80490e github.com/hajimehoshi/go-mp3 v0.3.2 - github.com/hajimehoshi/oto v0.7.1 github.com/jakecoffman/cp v1.1.0 github.com/jfreymuth/oggvorbis v1.0.3 golang.org/x/image v0.0.0-20210220032944-ac19c3e999fb diff --git a/go.sum b/go.sum index ed8a47597..390fbfdb8 100644 --- a/go.sum +++ b/go.sum @@ -10,8 +10,6 @@ github.com/hajimehoshi/file2byteslice v0.0.0-20200812174855-0e5e8a80490e/go.mod github.com/hajimehoshi/go-mp3 v0.3.2 h1:xSYNE2F3lxtOu9BRjCWHHceg7S91IHfXfXp5+LYQI7s= github.com/hajimehoshi/go-mp3 v0.3.2/go.mod h1:qMJj/CSDxx6CGHiZeCgbiq2DSUkbK0UbtXShQcnfyMM= github.com/hajimehoshi/oto v0.6.1/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/jakecoffman/cp v1.1.0 h1:bhKvCNbAddYegYHSV5abG3G23vZdsISgqXa4X/lK8Oo= github.com/jakecoffman/cp v1.1.0/go.mod h1:JjY/Fp6d8E1CHnu74gWNnU0+b9VzEdUVPoJxg2PsTQg= github.com/jfreymuth/oggvorbis v1.0.3 h1:MLNGGyhOMiVcvea9Dp5+gbs2SAwqwQbtrWnonYa0M0Y=