From c889feb669d552f87a4fb22d92684fee9ea85844 Mon Sep 17 00:00:00 2001 From: Hajime Hoshi Date: Tue, 25 May 2021 01:49:27 +0900 Subject: [PATCH] tmp --- .builds/alpine.yml | 4 +- .builds/arch.yml | 8 +- .builds/debian.yml | 2 +- .builds/fedora.yml | 8 +- .builds/ubuntu.yml | 2 +- .github/workflows/test.yml | 2 +- audio/internal/readerdriver/driver_default.go | 39 -- audio/internal/readerdriver/driver_unix.go | 453 ++++++++++++++++++ 8 files changed, 466 insertions(+), 52 deletions(-) delete mode 100644 audio/internal/readerdriver/driver_default.go create mode 100644 audio/internal/readerdriver/driver_unix.go diff --git a/.builds/alpine.yml b/.builds/alpine.yml index 01b0ff2fd..31802835f 100644 --- a/.builds/alpine.yml +++ b/.builds/alpine.yml @@ -1,13 +1,13 @@ image: alpine/latest packages: - - alsa-lib-dev - libx11-dev - - libxrandr-dev - libxcursor-dev - libxinerama-dev - libxi-dev + - libxrandr-dev - mesa-dev - pkgconf + - portaudio-dev - go sources: - https://github.com/hajimehoshi/ebiten diff --git a/.builds/arch.yml b/.builds/arch.yml index 9a86cf6fa..c377d63a3 100644 --- a/.builds/arch.yml +++ b/.builds/arch.yml @@ -1,12 +1,12 @@ image: archlinux packages: - - alsa-lib - - mesa - - libxrandr - libxcursor - - libxinerama - libxi + - libxinerama + - libxrandr + - mesa - pkg-config + - portaudio - go sources: - https://github.com/hajimehoshi/ebiten diff --git a/.builds/debian.yml b/.builds/debian.yml index 91dfc22c2..307682e68 100644 --- a/.builds/debian.yml +++ b/.builds/debian.yml @@ -8,8 +8,8 @@ packages: - libxinerama-dev - libxrandr-dev - libxxf86vm-dev - - libasound2-dev - pkg-config + - portaudio19-dev - golang sources: - https://github.com/hajimehoshi/ebiten diff --git a/.builds/fedora.yml b/.builds/fedora.yml index 7dfcf8d23..9c95e5ba8 100644 --- a/.builds/fedora.yml +++ b/.builds/fedora.yml @@ -1,14 +1,14 @@ image: fedora/latest packages: - - mesa-libGLU-devel - - mesa-libGLES-devel - - libXrandr-devel - libXcursor-devel + - libXrandr-devel - libXinerama-devel - libXi-devel - libXxf86vm-devel - - alsa-lib-devel + - mesa-libGLES-devel + - mesa-libGLU-devel - pkg-config + - portaudio-devel - go sources: - https://github.com/hajimehoshi/ebiten diff --git a/.builds/ubuntu.yml b/.builds/ubuntu.yml index c927289a1..61a362d62 100644 --- a/.builds/ubuntu.yml +++ b/.builds/ubuntu.yml @@ -8,8 +8,8 @@ packages: - libxinerama-dev - libxrandr-dev - libxxf86vm-dev - - libasound2-dev - pkg-config + - portaudio19-dev - golang sources: - https://github.com/hajimehoshi/ebiten diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index ef39218c3..2b77cbdb6 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -35,7 +35,7 @@ jobs: if: ${{ matrix.os == 'ubuntu-latest' }} run: | sudo apt-get update - sudo apt-get install libasound2-dev libgl1-mesa-dev libalut-dev libxcursor-dev libxi-dev libxinerama-dev libxrandr-dev libxxf86vm-dev + sudo apt-get install libgl1-mesa-dev libxcursor-dev libxi-dev libxinerama-dev libxrandr-dev libxxf86vm-dev portaudio19-dev - name: Install wasmbrowsertest run: | diff --git a/audio/internal/readerdriver/driver_default.go b/audio/internal/readerdriver/driver_default.go deleted file mode 100644 index a84d894e7..000000000 --- a/audio/internal/readerdriver/driver_default.go +++ /dev/null @@ -1,39 +0,0 @@ -// 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. - -// +build !android -// +build !darwin -// +build !js -// +build !windows - -package readerdriver - -import ( - "fmt" - "runtime" -) - -func IsAvailable() bool { - return false -} - -type context struct { - sampleRate int - channelNum int - bitDepthInBytes int -} - -func NewContext(sampleRate int, channelNum int, bitDepthInBytes int) (Context, chan struct{}, error) { - panic(fmt.Sprintf("readerdriver: NewContext is not available on this environment: GOOS=%s", runtime.GOOS)) -} diff --git a/audio/internal/readerdriver/driver_unix.go b/audio/internal/readerdriver/driver_unix.go new file mode 100644 index 000000000..c8a0269e9 --- /dev/null +++ b/audio/internal/readerdriver/driver_unix.go @@ -0,0 +1,453 @@ +// 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. + +// +build aix dragonfly freebsd hurd illumos linux netbsd openbsd solaris +// +build !android + +package readerdriver + +// #cgo pkg-config: portaudio-2.0 +// #cgo LDFLAGS: -lportaudio +// +// #include "portaudio.h" +// +// int ebiten_readerdriver_streamCallback(void *input, void *output, unsigned long frameCount, PaStreamCallbackTimeInfo *timeInfo, PaStreamCallbackFlags statusFlags, void *userData); +import "C" + +import ( + "fmt" + "io" + "runtime" + "sync" + "unsafe" +) + +type paError struct { + value C.PaError + fname string +} + +func (e *paError) Error() string { + return fmt.Sprintf("PortAudio %s failed: %s", e.fname, C.GoString(C.Pa_GetErrorText(e.value))) +} + +func paErrorToError(err C.PaError, fname string) error { + if err == C.paNoError { + return nil + } + return &paError{ + value: err, + fname: fname, + } +} + +func IsAvailable() bool { + return true +} + +type context struct { + sampleRate int + channelNum int + bitDepthInBytes int + + stream unsafe.Pointer // *C.PaStream + players map[*playerImpl]struct{} + buf []float32 + m sync.Mutex +} + +func NewContext(sampleRate, channelNum, bitDepthInBytes int) (Context, chan struct{}, error) { + ready := make(chan struct{}) + close(ready) + + c := &context{ + sampleRate: sampleRate, + channelNum: channelNum, + bitDepthInBytes: bitDepthInBytes, + } + + if err := paErrorToError(C.Pa_Initialize(), "Pa_Initialize"); err != nil { + return nil, nil, err + } + + if err := paErrorToError(C.Pa_OpenDefaultStream( + &c.stream, + 0, + C.int(c.channelNum), + C.paFloat32, + C.double(c.sampleRate), + C.paFramesPerBufferUnspecified, + (*C.PaStreamCallback)(C.ebiten_readerdriver_streamCallback), + unsafe.Pointer(c), + ), "Pa_OpenDefaultStream"); err != nil { + return nil, nil, err + } + + if err := paErrorToError(C.Pa_StartStream(c.stream), "Pa_StartStream"); err != nil { + return nil, nil, err + } + + return c, ready, nil +} + +func (c *context) Suspend() error { + if err := paErrorToError(C.Pa_StopStream(c.stream), "Pa_StopStream"); err != nil { + return err + } + return nil +} + +func (c *context) Resume() error { + if err := paErrorToError(C.Pa_StartStream(c.stream), "Pa_StartStream"); err != nil { + return err + } + return nil +} + +func (c *context) addPlayer(player *playerImpl) { + c.m.Lock() + defer c.m.Unlock() + + if c.players == nil { + c.players = map[*playerImpl]struct{}{} + } + c.players[player] = struct{}{} +} + +func (c *context) removePlayer(player *playerImpl) { + c.m.Lock() + defer c.m.Unlock() + delete(c.players, player) +} + +//export ebiten_readerdriver_streamCallback +func ebiten_readerdriver_streamCallback(input, output unsafe.Pointer, frameCount C.ulong, timeInfo *C.PaStreamCallbackTimeInfo, statusFlags C.PaStreamCallbackFlags, userData unsafe.Pointer) C.int { + c := (*context)(userData) + + var n int + if statusFlags&(C.paOutputUnderflow|C.paOutputOverflow) == 0 { + n = int(frameCount) * c.channelNum + } + + if len(c.buf) < n { + c.buf = make([]float32, n) + } else { + for i := 0; i < n; i++ { + c.buf[i] = 0 + } + } + + if n > 0 { + c.m.Lock() + players := make([]*playerImpl, 0, len(c.players)) + for p := range c.players { + players = append(players, p) + } + c.m.Unlock() + + for _, p := range players { + p.addBuffer(c.buf[:n]) + } + for i := uintptr(0); i < uintptr(n); i++ { + *(*float32)(unsafe.Pointer(uintptr(output) + 4*i)) = c.buf[i] + } + } + + for i := uintptr(n); i < uintptr(frameCount); i++ { + *(*float32)(unsafe.Pointer(uintptr(output) + 4*i)) = 0 + } + return C.paContinue +} + +type player struct { + p *playerImpl +} + +type playerImpl struct { + context *context + src io.Reader + cond *sync.Cond + volume float64 + err error + state playerState + buf []byte + hasLoop bool +} + +func (c *context) NewPlayer(src io.Reader) Player { + p := &player{ + p: &playerImpl{ + context: c, + src: src, + cond: sync.NewCond(&sync.Mutex{}), + volume: 1, + }, + } + runtime.SetFinalizer(p, (*player).Close) + return p +} + +func (p *player) Err() error { + return p.p.Err() +} + +func (p *playerImpl) Err() error { + p.cond.L.Lock() + defer p.cond.L.Unlock() + + return p.err +} + +func (p *player) Play() { + p.p.Play() +} + +func (p *playerImpl) Play() { + ch := make(chan struct{}) + go func() { + p.cond.L.Lock() + defer p.cond.L.Unlock() + close(ch) + p.playImpl() + }() + <-ch +} + +func (p *playerImpl) playImpl() { + if p.err != nil { + return + } + if p.state != playerPaused { + return + } + + buf := make([]byte, p.context.maxBufferSize()) + for len(p.buf) < p.context.maxBufferSize() { + 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 { + break + } + } + + p.state = playerPlay + + p.cond.L.Unlock() + p.context.addPlayer(p) + p.cond.L.Lock() + + p.cond.Signal() + + if !p.hasLoop { + go p.loop() + p.hasLoop = true + } +} + +func (p *player) Pause() { + p.p.Pause() +} + +func (p *playerImpl) Pause() { + p.cond.L.Lock() + defer p.cond.L.Unlock() + p.pauseImpl() +} + +func (p *playerImpl) pauseImpl() { + if p.state != playerPlay { + return + } + p.state = playerPaused + p.cond.Signal() +} + +func (p *player) Reset() { + p.p.Reset() +} + +func (p *playerImpl) Reset() { + p.cond.L.Lock() + defer p.cond.L.Unlock() + p.resetImpl() +} + +func (p *playerImpl) resetImpl() { + if p.state == playerClosed { + return + } + p.state = playerPaused + p.buf = p.buf[:0] + p.cond.Signal() +} + +func (p *player) IsPlaying() bool { + return p.p.IsPlaying() +} + +func (p *playerImpl) IsPlaying() bool { + p.cond.L.Lock() + defer p.cond.L.Unlock() + return p.state == playerPlay +} + +func (p *player) Volume() float64 { + return p.p.Volume() +} + +func (p *playerImpl) Volume() float64 { + p.cond.L.Lock() + defer p.cond.L.Unlock() + return p.volume +} + +func (p *player) SetVolume(volume float64) { + p.p.SetVolume(volume) +} + +func (p *playerImpl) SetVolume(volume float64) { + p.cond.L.Lock() + defer p.cond.L.Unlock() + p.volume = volume +} + +func (p *player) UnplayedBufferSize() int { + return p.p.UnplayedBufferSize() +} + +func (p *playerImpl) UnplayedBufferSize() int { + p.cond.L.Lock() + defer p.cond.L.Unlock() + return len(p.buf) +} + +func (p *player) Close() error { + runtime.SetFinalizer(p, nil) + return p.p.Close() +} + +func (p *playerImpl) Close() error { + p.cond.L.Lock() + defer p.cond.L.Unlock() + return p.closeImpl() +} + +func (p *playerImpl) closeImpl() error { + p.cond.L.Unlock() + p.context.removePlayer(p) + p.cond.L.Lock() + + if p.state == playerClosed { + return nil + } + p.state = playerClosed + p.buf = nil + p.cond.Signal() + return p.err +} + +func (p *playerImpl) addBuffer(buf []float32) int { + p.cond.L.Lock() + defer p.cond.L.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) + for i := 0; i < n; i++ { + var v float32 + switch bitDepthInBytes { + case 1: + v8 := p.buf[i] + v = float32(v8-(1<<7)) / (1 << 7) + case 2: + v16 := int16(p.buf[2*i]) | (int16(p.buf[2*i+1]) << 8) + v = float32(v16) / (1 << 15) + } + buf[i] += v * volume + } + p.buf = p.buf[n*bitDepthInBytes:] + if n > 0 { + p.cond.Signal() + } + return n +} + +func (p *playerImpl) shouldWait() bool { + switch p.state { + case playerPaused: + return true + case playerPlay: + // If the buffer has too much data, wait until the buffer data is consumed. + // If the source reaches EOF, wait until the state is reset. + return len(p.buf) >= p.context.maxBufferSize() + case playerClosed: + return false + default: + panic("not reached") + } +} + +func (p *playerImpl) wait() bool { + p.cond.L.Lock() + defer p.cond.L.Unlock() + + for p.shouldWait() { + p.cond.Wait() + } + return p.state == playerPlay +} + +func (p *playerImpl) setError(err error) { + p.cond.L.Lock() + defer p.cond.L.Unlock() + p.setErrorImpl(err) +} + +func (p *playerImpl) setErrorImpl(err error) { + p.err = err + p.closeImpl() +} + +func (p *playerImpl) loop() { + buf := make([]byte, 4096) + for { + if !p.wait() { + return + } + + n, err := p.src.Read(buf) + if err != nil && err != io.EOF { + p.setError(err) + return + } + + p.cond.L.Lock() + p.buf = append(p.buf, buf[:n]...) + if err == io.EOF && len(p.buf) == 0 { + p.resetImpl() + } + p.cond.L.Unlock() + } +}