This commit is contained in:
Hajime Hoshi 2021-05-25 01:49:27 +09:00
parent 3fb469d5a5
commit c889feb669
8 changed files with 466 additions and 52 deletions

View File

@ -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

View File

@ -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

View File

@ -8,8 +8,8 @@ packages:
- libxinerama-dev
- libxrandr-dev
- libxxf86vm-dev
- libasound2-dev
- pkg-config
- portaudio19-dev
- golang
sources:
- https://github.com/hajimehoshi/ebiten

View File

@ -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

View File

@ -8,8 +8,8 @@ packages:
- libxinerama-dev
- libxrandr-dev
- libxxf86vm-dev
- libasound2-dev
- pkg-config
- portaudio19-dev
- golang
sources:
- https://github.com/hajimehoshi/ebiten

View File

@ -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: |

View File

@ -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))
}

View File

@ -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()
}
}