From 011e5061dff06f9a9249faf5c062767524aebb15 Mon Sep 17 00:00:00 2001 From: Hajime Hoshi Date: Thu, 23 Dec 2021 03:46:26 +0900 Subject: [PATCH] internal/cbackend: simplify the audio API --- audio/internal/cbackend/context.go | 277 +------------------- audio/internal/cbackend/player.go | 400 +++++++++++++++++++++++++++++ internal/cbackend/cbackend.go | 106 ++------ 3 files changed, 427 insertions(+), 356 deletions(-) create mode 100644 audio/internal/cbackend/player.go diff --git a/audio/internal/cbackend/context.go b/audio/internal/cbackend/context.go index 2cef49fbf..111d1cf24 100644 --- a/audio/internal/cbackend/context.go +++ b/audio/internal/cbackend/context.go @@ -19,8 +19,6 @@ package cbackend import ( "io" - "runtime" - "sync" "github.com/hajimehoshi/ebiten/v2/internal/cbackend" ) @@ -29,31 +27,25 @@ type Context struct { sampleRate int channelNum int bitDepthInBytes int + + players *players } func NewContext(sampleRate, channelNum, bitDepthInBytes int) (*Context, chan struct{}, error) { - cbackend.OpenAudio(sampleRate, channelNum, bitDepthInBytes) c := &Context{ sampleRate: sampleRate, channelNum: channelNum, bitDepthInBytes: bitDepthInBytes, + players: newPlayers(), } + cbackend.OpenAudio(sampleRate, channelNum, c.players.read) ready := make(chan struct{}) close(ready) return c, ready, nil } -func (c *Context) NewPlayer(r io.Reader) *Player { - cond := sync.NewCond(&sync.Mutex{}) - p := &Player{ - context: c, - src: r, - volume: 1, - cond: cond, - onWritten: cond.Signal, - } - runtime.SetFinalizer(p, (*Player).Close) - return p +func (c *Context) NewPlayer(src io.Reader) *Player { + return newPlayer(c, src) } func (c *Context) Suspend() error { @@ -70,259 +62,6 @@ func (c *Context) Err() error { return nil } -func (c *Context) oneBufferSize() int { - return int(float64(c.sampleRate*c.channelNum*c.bitDepthInBytes) * cbackend.AudioBufferSizeInSeconds()) -} - -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 ( - playerStatePaused playerState = iota - playerStatePlaying - playerStateClosed -) - -type Player struct { - context *Context - src io.Reader - v *cbackend.AudioPlayer - state playerState - volume float64 - cond *sync.Cond - err error - buf []byte - - onWritten func() -} - -func (p *Player) Pause() { - p.cond.L.Lock() - defer p.cond.L.Unlock() - - if p.state == playerStateClosed { - return - } - if p.v == nil { - return - } - - p.v.Pause() - p.state = playerStatePaused - p.cond.Signal() -} - -func (p *Player) Play() { - p.cond.L.Lock() - defer p.cond.L.Unlock() - - if p.state == playerStateClosed { - return - } - - var runloop bool - if p.v == nil { - p.v = cbackend.CreateAudioPlayer(p.onWritten) - runloop = true - } - - p.v.SetVolume(p.volume) - p.v.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.context.oneBufferSize() - if max := p.context.MaxBufferSize() - p.UnplayedBufferSize(); n > max { - n = max - } - p.buf = make([]byte, n) - } - n, err := p.src.Read(p.buf) - if err != nil && err != io.EOF { - p.setErrorImpl(err) - return - } - if n > 0 { - p.writeImpl(p.buf[:n]) - } - - if runloop { - go p.loop() - } - p.state = playerStatePlaying - p.cond.Signal() -} - -func (p *Player) IsPlaying() bool { - p.cond.L.Lock() - defer p.cond.L.Unlock() - - return p.state == playerStatePlaying -} - -func (p *Player) Reset() { - p.cond.L.Lock() - defer p.cond.L.Unlock() - - if p.state == playerStateClosed { - return - } - p.state = playerStatePaused - - if p.v == nil { - return - } - - p.v.Close(true) - p.v = nil - p.cond.Signal() -} - -func (p *Player) Volume() float64 { - p.cond.L.Lock() - defer p.cond.L.Unlock() - - if p.v == nil { - return p.volume - } - return p.v.Volume() -} - -func (p *Player) SetVolume(volume float64) { - p.cond.L.Lock() - defer p.cond.L.Unlock() - - p.volume = volume - if p.v == nil { - return - } - p.v.SetVolume(volume) -} - -func (p *Player) UnplayedBufferSize() int { - if p.v == nil { - return 0 - } - return p.v.UnplayedBufferSize() -} - -func (p *Player) Err() error { - p.cond.L.Lock() - defer p.cond.L.Unlock() - - return p.err -} - -func (p *Player) Close() error { - runtime.SetFinalizer(p, nil) - return p.close(true) -} - -func (p *Player) close(remove bool) error { - p.cond.L.Lock() - defer p.cond.L.Unlock() - return p.closeImpl(remove) -} - -func (p *Player) closeImpl(remove bool) error { - if p.state == playerStateClosed { - return p.err - } - - if p.v != nil { - p.v.Close(false) - p.v = nil - } - if remove { - p.state = playerStateClosed - p.onWritten = nil - } else { - p.state = playerStatePaused - } - p.cond.Signal() - return p.err -} - -func (p *Player) setError(err error) { - p.cond.L.Lock() - defer p.cond.L.Unlock() - p.setErrorImpl(err) -} - -func (p *Player) setErrorImpl(err error) { - if p.state != playerStateClosed && p.v != nil { - p.v.Close(true) - p.v = nil - } - p.err = err - p.state = playerStateClosed - p.cond.Signal() -} - -func (p *Player) shouldWait() bool { - if p.v == nil { - return false - } - switch p.state { - case playerStatePaused: - return true - case playerStatePlaying: - return p.v.UnplayedBufferSize() >= p.context.MaxBufferSize() - } - return false -} - -func (p *Player) waitUntilUnpaused() bool { - p.cond.L.Lock() - defer p.cond.L.Unlock() - - for p.shouldWait() { - p.cond.Wait() - } - return p.v != nil && p.state == playerStatePlaying -} - -func (p *Player) writeImpl(buf []byte) { - if p.state == playerStateClosed { - return - } - if p.v == nil { - return - } - p.v.Write(buf) -} - -func (p *Player) loop() { - const readChunkSize = 4096 - - buf := make([]byte, readChunkSize) - - for { - if !p.waitUntilUnpaused() { - return - } - - p.cond.L.Lock() - n, err := p.src.Read(buf) - if err != nil && err != io.EOF { - p.setErrorImpl(err) - p.cond.L.Unlock() - return - } - if n > 0 { - p.writeImpl(buf[:n]) - } - - if err == io.EOF { - p.closeImpl(false) - p.cond.L.Unlock() - return - } - p.cond.L.Unlock() - } +func (c *Context) bufferSize() int { + return c.sampleRate * c.channelNum * c.bitDepthInBytes / 4 // 0.25[s] } diff --git a/audio/internal/cbackend/player.go b/audio/internal/cbackend/player.go new file mode 100644 index 000000000..edf64e423 --- /dev/null +++ b/audio/internal/cbackend/player.go @@ -0,0 +1,400 @@ +// 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. + +//go:build ebitencbackend +// +build ebitencbackend + +// TODO: This implementation is very similar to github.com/hajimehoshi/oto/v2's player_notjs.go +// Unify them if possible. + +package cbackend + +import ( + "io" + "runtime" + "sync" + "sync/atomic" +) + +type playerState int + +const ( + playerPaused playerState = iota + playerPlay + playerClosed +) + +type players struct { + players map[*playerImpl]struct{} + buf []float32 + cond *sync.Cond +} + +func newPlayers() *players { + p := &players{ + cond: sync.NewCond(&sync.Mutex{}), + } + go p.loop() + return p +} + +func (ps *players) shouldWait() bool { + for p := range ps.players { + if p.canReadSourceToBuffer() { + return false + } + } + return true +} + +func (ps *players) wait() { + ps.cond.L.Lock() + defer ps.cond.L.Unlock() + + for ps.shouldWait() { + ps.cond.Wait() + } +} + +func (ps *players) loop() { + var players []*playerImpl + for { + ps.wait() + + ps.cond.L.Lock() + players = players[:0] + for p := range ps.players { + players = append(players, p) + } + ps.cond.L.Unlock() + + for _, p := range players { + p.readSourceToBuffer() + } + } +} + +func (ps *players) addPlayer(player *playerImpl) { + ps.cond.L.Lock() + defer ps.cond.L.Unlock() + + if ps.players == nil { + ps.players = map[*playerImpl]struct{}{} + } + ps.players[player] = struct{}{} + ps.cond.Signal() +} + +func (ps *players) removePlayer(player *playerImpl) { + ps.cond.L.Lock() + defer ps.cond.L.Unlock() + + delete(ps.players, player) + ps.cond.Signal() +} + +func (ps *players) read(buf []float32) { + ps.cond.L.Lock() + players := make([]*playerImpl, 0, len(ps.players)) + for p := range ps.players { + players = append(players, p) + } + ps.cond.L.Unlock() + + for i := range buf { + buf[i] = 0 + } + for _, p := range players { + p.readBufferAndAdd(buf) + } + ps.cond.Signal() +} + +type Player struct { + p *playerImpl +} + +type playerImpl struct { + context *Context + src io.Reader + volume float64 + err atomic.Value + state playerState + tmpbuf []byte + buf []byte + eof bool + + m sync.Mutex +} + +func newPlayer(context *Context, src io.Reader) *Player { + p := &Player{ + p: &playerImpl{ + context: context, + src: src, + volume: 1, + }, + } + runtime.SetFinalizer(p, (*Player).Close) + return p +} + +func (p *Player) Err() error { + return p.p.Err() +} + +func (p *playerImpl) Err() error { + if err := p.err.Load(); err != nil { + return err.(error) + } + return nil +} + +func (p *Player) Play() { + p.p.Play() +} + +func (p *playerImpl) Play() { + ch := make(chan struct{}) + go func() { + p.m.Lock() + defer p.m.Unlock() + + close(ch) + p.playImpl() + }() + <-ch +} + +func (p *playerImpl) ensureTmpBuf() []byte { + if p.tmpbuf == nil { + p.tmpbuf = make([]byte, p.context.bufferSize()) + } + return p.tmpbuf +} + +func (p *playerImpl) playImpl() { + if p.err.Load() != nil { + return + } + if p.state != playerPaused { + return + } + + if !p.eof { + buf := p.ensureTmpBuf() + for len(p.buf) < p.context.bufferSize() { + n, err := p.src.Read(buf) + if err != nil && err != io.EOF { + p.setErrorImpl(err) + return + } + p.buf = append(p.buf, buf[:n]...) + if err == io.EOF { + if len(p.buf) == 0 { + p.eof = true + } + break + } + } + } + + if !p.eof || len(p.buf) > 0 { + p.state = playerPlay + } + + p.m.Unlock() + p.context.players.addPlayer(p) + p.m.Lock() +} + +func (p *Player) Pause() { + p.p.Pause() +} + +func (p *playerImpl) Pause() { + p.m.Lock() + defer p.m.Unlock() + + if p.state != playerPlay { + return + } + p.state = playerPaused +} + +func (p *Player) Reset() { + p.p.Reset() +} + +func (p *playerImpl) Reset() { + p.m.Lock() + defer p.m.Unlock() + p.resetImpl() +} + +func (p *playerImpl) resetImpl() { + if p.state == playerClosed { + return + } + p.state = playerPaused + p.buf = p.buf[:0] + p.eof = false +} + +func (p *Player) IsPlaying() bool { + return p.p.IsPlaying() +} + +func (p *playerImpl) IsPlaying() bool { + p.m.Lock() + defer p.m.Unlock() + return p.state == playerPlay +} + +func (p *Player) Volume() float64 { + return p.p.Volume() +} + +func (p *playerImpl) Volume() float64 { + p.m.Lock() + defer p.m.Unlock() + return p.volume +} + +func (p *Player) SetVolume(volume float64) { + p.p.SetVolume(volume) +} + +func (p *playerImpl) SetVolume(volume float64) { + p.m.Lock() + defer p.m.Unlock() + p.volume = volume +} + +func (p *Player) UnplayedBufferSize() int { + return p.p.UnplayedBufferSize() +} + +func (p *playerImpl) UnplayedBufferSize() int { + p.m.Lock() + defer p.m.Unlock() + return len(p.buf) +} + +func (p *Player) Close() error { + runtime.SetFinalizer(p, nil) + return p.p.Close() +} + +func (p *playerImpl) Close() error { + p.m.Lock() + defer p.m.Unlock() + return p.closeImpl() +} + +func (p *playerImpl) closeImpl() error { + p.m.Unlock() + p.context.players.removePlayer(p) + p.m.Lock() + + if p.state == playerClosed { + return nil + } + p.state = playerClosed + p.buf = nil + if err := p.err.Load(); err != nil { + return err.(error) + } + return nil +} + +func (p *playerImpl) readBufferAndAdd(buf []float32) int { + p.m.Lock() + defer p.m.Unlock() + + if p.state != playerPlay { + return 0 + } + + bitDepthInBytes := p.context.bitDepthInBytes + n := len(p.buf) / bitDepthInBytes + if n > len(buf) { + n = len(buf) + } + volume := float32(p.volume) + src := p.buf[:n*bitDepthInBytes] + + for i := 0; i < n; i++ { + var v float32 + switch bitDepthInBytes { + case 1: + v8 := src[i] + v = float32(v8-(1<<7)) / (1 << 7) + case 2: + v16 := int16(src[2*i]) | (int16(src[2*i+1]) << 8) + v = float32(v16) / (1 << 15) + } + buf[i] += v * volume + } + + copy(p.buf, p.buf[n*bitDepthInBytes:]) + p.buf = p.buf[:len(p.buf)-n*bitDepthInBytes] + + return n +} + +func (p *playerImpl) canReadSourceToBuffer() bool { + p.m.Lock() + defer p.m.Unlock() + + if p.eof { + return false + } + return len(p.buf) < p.context.bufferSize() +} + +func (p *playerImpl) readSourceToBuffer() { + p.m.Lock() + defer p.m.Unlock() + + if p.err.Load() != nil { + return + } + if p.state == playerClosed { + return + } + + if len(p.buf) >= p.context.bufferSize() { + return + } + + buf := p.ensureTmpBuf() + n, err := p.src.Read(buf) + + if err != nil && err != io.EOF { + p.setErrorImpl(err) + return + } + + p.buf = append(p.buf, buf[:n]...) + if err == io.EOF && len(p.buf) == 0 { + p.state = playerPaused + p.eof = true + } +} + +func (p *playerImpl) setErrorImpl(err error) { + p.err.Store(err) + p.closeImpl() +} diff --git a/internal/cbackend/cbackend.go b/internal/cbackend/cbackend.go index 16b4d09ce..1e8a4ddd4 100644 --- a/internal/cbackend/cbackend.go +++ b/internal/cbackend/cbackend.go @@ -51,29 +51,18 @@ package cbackend // void EbitenGetTouches(struct Touch* touches); // // // Audio -// // TODO: Implement mixing on Go side and reduce the API. -// typedef void (*OnWrittenCallback)(int id); -// void EbitenOpenAudio(int sample_rate, int channel_num, int bit_depth_in_bytes); +// typedef void (*OnReadCallback)(float* buf, size_t length); +// void EbitenOpenAudio(int sample_rate, int channel_num, OnReadCallback on_read_callback); // void EbitenCloseAudio(); -// int EbitenCreateAudioPlayer(OnWrittenCallback on_written_callback); -// void EbitenAudioPlayerPlay(int id); -// void EbitenAudioPlayerPause(int id); -// void EbitenAudioPlayerWrite(int id, uint8_t* data, int length); -// void EbitenAudioPlayerClose(int id, int immediately); -// double EbitenAudioPlayerGetVolume(int id); -// void EbitenAudioPlayerSetVolume(int id, double volume); -// int EbitenAudioPlayerGetUnplayedBufferSize(int id); -// float EbitenAudioBufferSizeInSeconds(); // -// void EbitenAudioPlayerOnWrittenCallback(int id); -// static int EbitenCreateAudioPlayerProxy() { -// return EbitenCreateAudioPlayer(EbitenAudioPlayerOnWrittenCallback); +// void EbitenAudioOnReadCallback(float* buf, size_t length); +// static void EbitenOpenAudioProxy(int sample_rate, int channel_num) { +// EbitenOpenAudio(sample_rate, channel_num, EbitenAudioOnReadCallback); // } import "C" import ( - "runtime" - "sync" + "reflect" "unsafe" "github.com/hajimehoshi/ebiten/v2/internal/driver" @@ -170,80 +159,23 @@ func AppendTouches(touches []Touch) []Touch { return touches } -func OpenAudio(sampleRate, channelNum, bitDepthInBytes int) { - C.EbitenOpenAudio(C.int(sampleRate), C.int(channelNum), C.int(bitDepthInBytes)) +var onReadCallback func(buf []float32) + +func OpenAudio(sampleRate, channelNum int, onRead func(buf []float32)) { + C.EbitenOpenAudioProxy(C.int(sampleRate), C.int(channelNum)) + onReadCallback = onRead } func CloseAudio() { C.EbitenCloseAudio() } -func CreateAudioPlayer(onWritten func()) *AudioPlayer { - id := C.EbitenCreateAudioPlayerProxy() - p := &AudioPlayer{ - id: id, - } - onWrittenCallbacksM.Lock() - defer onWrittenCallbacksM.Unlock() - onWrittenCallbacks[id] = onWritten - return p -} - -func AudioBufferSizeInSeconds() float64 { - return float64(C.EbitenAudioBufferSizeInSeconds()) -} - -type AudioPlayer struct { - id C.int -} - -func (p *AudioPlayer) Play() { - C.EbitenAudioPlayerPlay(p.id) -} - -func (p *AudioPlayer) Pause() { - C.EbitenAudioPlayerPause(p.id) -} - -func (p *AudioPlayer) Write(buf []byte) { - C.EbitenAudioPlayerWrite(p.id, (*C.uint8_t)(unsafe.Pointer(&buf[0])), C.int(len(buf))) - runtime.KeepAlive(buf) -} - -func (p *AudioPlayer) Close(immediately bool) { - var i C.int - if immediately { - i = 1 - } - C.EbitenAudioPlayerClose(p.id, i) - - onWrittenCallbacksM.Lock() - defer onWrittenCallbacksM.Unlock() - delete(onWrittenCallbacks, p.id) -} - -func (p *AudioPlayer) Volume() float64 { - return float64(C.EbitenAudioPlayerGetVolume(p.id)) -} - -func (p *AudioPlayer) SetVolume(volume float64) { - C.EbitenAudioPlayerSetVolume(p.id, C.double(volume)) -} - -func (p *AudioPlayer) UnplayedBufferSize() int { - return int(C.EbitenAudioPlayerGetUnplayedBufferSize(p.id)) -} - -var ( - onWrittenCallbacks = map[C.int]func(){} - onWrittenCallbacksM sync.Mutex -) - -//export EbitenAudioPlayerOnWrittenCallback -func EbitenAudioPlayerOnWrittenCallback(id C.int) { - onWrittenCallbacksM.Lock() - defer onWrittenCallbacksM.Unlock() - if c, ok := onWrittenCallbacks[id]; ok { - c() - } +//export EbitenAudioOnReadCallback +func EbitenAudioOnReadCallback(buf *C.float, length C.size_t) { + var s []float32 + h := (*reflect.SliceHeader)(unsafe.Pointer(&s)) + h.Data = uintptr(unsafe.Pointer(buf)) + h.Len = int(length) + h.Cap = int(length) + onReadCallback(s) }