mirror of
https://github.com/hajimehoshi/ebiten.git
synced 2024-12-26 19:58:54 +01:00
audio/internal/readerdriver: Use ALSA instead of PulseAudio
Updates #1633 Closes #1761 Updates #1766
This commit is contained in:
parent
3dcb3d7ea7
commit
13e84f0224
@ -8,7 +8,6 @@ packages:
|
|||||||
- libxrandr-dev
|
- libxrandr-dev
|
||||||
- mesa-dev
|
- mesa-dev
|
||||||
- pkgconf
|
- pkgconf
|
||||||
- pulseaudio-dev
|
|
||||||
- go
|
- go
|
||||||
sources:
|
sources:
|
||||||
- https://github.com/hajimehoshi/ebiten
|
- https://github.com/hajimehoshi/ebiten
|
||||||
|
@ -1,8 +1,7 @@
|
|||||||
image: archlinux
|
image: archlinux
|
||||||
packages:
|
packages:
|
||||||
- alsa
|
- alsa-lib
|
||||||
- libxcursor
|
- libxcursor
|
||||||
- libpulse
|
|
||||||
- libxi
|
- libxi
|
||||||
- libxinerama
|
- libxinerama
|
||||||
- libxrandr
|
- libxrandr
|
||||||
|
@ -4,7 +4,6 @@ packages:
|
|||||||
- libc6-dev
|
- libc6-dev
|
||||||
- libglu1-mesa-dev
|
- libglu1-mesa-dev
|
||||||
- libgl1-mesa-dev
|
- libgl1-mesa-dev
|
||||||
- libpulse-dev
|
|
||||||
- libxcursor-dev
|
- libxcursor-dev
|
||||||
- libxi-dev
|
- libxi-dev
|
||||||
- libxinerama-dev
|
- libxinerama-dev
|
||||||
|
@ -9,7 +9,6 @@ packages:
|
|||||||
- mesa-libGLES-devel
|
- mesa-libGLES-devel
|
||||||
- mesa-libGLU-devel
|
- mesa-libGLU-devel
|
||||||
- pkg-config
|
- pkg-config
|
||||||
- pulseaudio-libs-devel
|
|
||||||
- go
|
- go
|
||||||
sources:
|
sources:
|
||||||
- https://github.com/hajimehoshi/ebiten
|
- https://github.com/hajimehoshi/ebiten
|
||||||
|
@ -4,7 +4,6 @@ packages:
|
|||||||
- libc6-dev
|
- libc6-dev
|
||||||
- libglu1-mesa-dev
|
- libglu1-mesa-dev
|
||||||
- libgl1-mesa-dev
|
- libgl1-mesa-dev
|
||||||
- libpulse-dev
|
|
||||||
- libxcursor-dev
|
- libxcursor-dev
|
||||||
- libxi-dev
|
- libxi-dev
|
||||||
- libxinerama-dev
|
- libxinerama-dev
|
||||||
|
2
.github/workflows/test.yml
vendored
2
.github/workflows/test.yml
vendored
@ -35,7 +35,7 @@ jobs:
|
|||||||
if: ${{ matrix.os == 'ubuntu-latest' }}
|
if: ${{ matrix.os == 'ubuntu-latest' }}
|
||||||
run: |
|
run: |
|
||||||
sudo apt-get update
|
sudo apt-get update
|
||||||
sudo apt-get install libasound2-dev libgl1-mesa-dev libpulse-dev libxcursor-dev libxi-dev libxinerama-dev libxrandr-dev libxxf86vm-dev
|
sudo apt-get install libasound2-dev libgl1-mesa-dev libxcursor-dev libxi-dev libxinerama-dev libxrandr-dev libxxf86vm-dev
|
||||||
|
|
||||||
- name: Install wasmbrowsertest
|
- name: Install wasmbrowsertest
|
||||||
run: |
|
run: |
|
||||||
|
@ -17,20 +17,14 @@
|
|||||||
|
|
||||||
package readerdriver
|
package readerdriver
|
||||||
|
|
||||||
// #cgo pkg-config: libpulse
|
// #cgo pkg-config: alsa
|
||||||
// #cgo LDFLAGS: -lpulse
|
|
||||||
//
|
//
|
||||||
// #include <pulse/pulseaudio.h>
|
// #include <alsa/asoundlib.h>
|
||||||
//
|
|
||||||
// void ebiten_readerdriver_contextStateCallback(pa_context *context, void *userdata);
|
|
||||||
// void ebiten_readerdriver_streamWriteCallback(pa_stream *stream, size_t requested_bytes, void *userdata);
|
|
||||||
// void ebiten_readerdriver_streamStateCallback(pa_stream *stream, void *userdata);
|
|
||||||
// void ebiten_readerdriver_streamSuccessCallback(pa_stream *stream, void *userdata);
|
|
||||||
import "C"
|
import "C"
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"runtime"
|
"time"
|
||||||
"unsafe"
|
"unsafe"
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -43,16 +37,17 @@ type context struct {
|
|||||||
channelNum int
|
channelNum int
|
||||||
bitDepthInBytes int
|
bitDepthInBytes int
|
||||||
|
|
||||||
mainloop *C.pa_threaded_mainloop
|
handle *C.snd_pcm_t
|
||||||
context *C.pa_context
|
supportsPause bool
|
||||||
stream *C.pa_stream
|
|
||||||
|
|
||||||
players *players
|
players *players
|
||||||
}
|
}
|
||||||
|
|
||||||
var theContext *context
|
var theContext *context
|
||||||
|
|
||||||
const bufferSize = 4096
|
func alsaError(err C.int) error {
|
||||||
|
return fmt.Errorf("readerdriver: ALSA error: %s", C.GoString(C.snd_strerror(err)))
|
||||||
|
}
|
||||||
|
|
||||||
func NewContext(sampleRate, channelNum, bitDepthInBytes int) (Context, chan struct{}, error) {
|
func NewContext(sampleRate, channelNum, bitDepthInBytes int) (Context, chan struct{}, error) {
|
||||||
ready := make(chan struct{})
|
ready := make(chan struct{})
|
||||||
@ -66,146 +61,126 @@ func NewContext(sampleRate, channelNum, bitDepthInBytes int) (Context, chan stru
|
|||||||
}
|
}
|
||||||
theContext = c
|
theContext = c
|
||||||
|
|
||||||
c.mainloop = C.pa_threaded_mainloop_new()
|
// Open a default ALSA audio device for blocking stream playback
|
||||||
if c.mainloop == nil {
|
cname := C.CString("default")
|
||||||
return nil, nil, fmt.Errorf("readerdriver: pa_threaded_mainloop_new failed")
|
defer C.free(unsafe.Pointer(cname))
|
||||||
}
|
if err := C.snd_pcm_open(&c.handle, cname, C.SND_PCM_STREAM_PLAYBACK, 0); err < 0 {
|
||||||
mainloopAPI := C.pa_threaded_mainloop_get_api(c.mainloop)
|
return nil, nil, alsaError(err)
|
||||||
if mainloopAPI == nil {
|
|
||||||
return nil, nil, fmt.Errorf("readerdriver: pa_threaded_mainloop_get_api failed")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
contextName := C.CString("pcm-playback")
|
periodSize := C.snd_pcm_uframes_t(1024)
|
||||||
defer C.free(unsafe.Pointer(contextName))
|
bufferSize := periodSize * 2
|
||||||
c.context = C.pa_context_new(mainloopAPI, contextName)
|
if err := c.alsaPcmHwParams(sampleRate, channelNum, &bufferSize, &periodSize); err != nil {
|
||||||
if c.context == nil {
|
return nil, nil, err
|
||||||
return nil, nil, fmt.Errorf("readerdriver: pa_context_new failed")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
C.pa_context_set_state_callback(c.context, C.pa_context_notify_cb_t(C.ebiten_readerdriver_contextStateCallback), unsafe.Pointer(c.mainloop))
|
go func() {
|
||||||
|
buf32 := make([]float32, int(periodSize)*c.channelNum)
|
||||||
runtime.LockOSThread()
|
for {
|
||||||
defer runtime.UnlockOSThread()
|
if err := c.readAndWrite(buf32); err != nil {
|
||||||
C.pa_threaded_mainloop_lock(c.mainloop)
|
panic(err)
|
||||||
defer C.pa_threaded_mainloop_unlock(c.mainloop)
|
}
|
||||||
|
|
||||||
if code := C.pa_threaded_mainloop_start(c.mainloop); code != 0 {
|
|
||||||
return nil, nil, fmt.Errorf("readerdriver: pa_threaded_mainloop_start failed: %s", C.GoString(C.pa_strerror(code)))
|
|
||||||
}
|
|
||||||
if code := C.pa_context_connect(c.context, nil, C.PA_CONTEXT_NOAUTOSPAWN, nil); code != 0 {
|
|
||||||
return nil, nil, fmt.Errorf("readerdriver: pa_context_connect failed: %s", C.GoString(C.pa_strerror(code)))
|
|
||||||
}
|
|
||||||
|
|
||||||
// Wait until the context is ready.
|
|
||||||
for {
|
|
||||||
contextState := C.pa_context_get_state(c.context)
|
|
||||||
if C.PA_CONTEXT_IS_GOOD(contextState) == 0 {
|
|
||||||
return nil, nil, fmt.Errorf("readerdriver: context state is bad")
|
|
||||||
}
|
}
|
||||||
if contextState == C.PA_CONTEXT_READY {
|
}()
|
||||||
break
|
|
||||||
}
|
|
||||||
C.pa_threaded_mainloop_wait(c.mainloop)
|
|
||||||
}
|
|
||||||
|
|
||||||
sampleSpecificatiom := C.pa_sample_spec{
|
|
||||||
format: C.PA_SAMPLE_FLOAT32LE,
|
|
||||||
rate: C.uint(sampleRate),
|
|
||||||
channels: C.uchar(channelNum),
|
|
||||||
}
|
|
||||||
var m C.pa_channel_map
|
|
||||||
switch channelNum {
|
|
||||||
case 1:
|
|
||||||
C.pa_channel_map_init_mono(&m)
|
|
||||||
case 2:
|
|
||||||
C.pa_channel_map_init_stereo(&m)
|
|
||||||
}
|
|
||||||
|
|
||||||
streamName := C.CString("Playback")
|
|
||||||
defer C.free(unsafe.Pointer(streamName))
|
|
||||||
c.stream = C.pa_stream_new(c.context, streamName, &sampleSpecificatiom, &m)
|
|
||||||
C.pa_stream_set_state_callback(c.stream, C.pa_stream_notify_cb_t(C.ebiten_readerdriver_streamStateCallback), unsafe.Pointer(c.mainloop))
|
|
||||||
C.pa_stream_set_write_callback(c.stream, C.pa_stream_request_cb_t(C.ebiten_readerdriver_streamWriteCallback), nil)
|
|
||||||
|
|
||||||
const defaultValue = 0xffffffff
|
|
||||||
bufferAttr := C.pa_buffer_attr{
|
|
||||||
maxlength: defaultValue,
|
|
||||||
tlength: bufferSize,
|
|
||||||
prebuf: defaultValue,
|
|
||||||
minreq: defaultValue,
|
|
||||||
}
|
|
||||||
var streamFlags C.pa_stream_flags_t = C.PA_STREAM_START_CORKED | C.PA_STREAM_INTERPOLATE_TIMING |
|
|
||||||
C.PA_STREAM_NOT_MONOTONIC | C.PA_STREAM_AUTO_TIMING_UPDATE |
|
|
||||||
C.PA_STREAM_ADJUST_LATENCY
|
|
||||||
|
|
||||||
if code := C.pa_stream_connect_playback(c.stream, nil, &bufferAttr, streamFlags, nil, nil); code != 0 {
|
|
||||||
return nil, nil, fmt.Errorf("readerdriver: pa_stream_connect_playback failed: %s", C.GoString(C.pa_strerror(code)))
|
|
||||||
}
|
|
||||||
|
|
||||||
// Wait until the stream is ready.
|
|
||||||
for {
|
|
||||||
streamState := C.pa_stream_get_state(c.stream)
|
|
||||||
if C.PA_STREAM_IS_GOOD(streamState) == 0 {
|
|
||||||
return nil, nil, fmt.Errorf("readerdriver: stream state is bad")
|
|
||||||
}
|
|
||||||
if streamState == C.PA_STREAM_READY {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
C.pa_threaded_mainloop_wait(c.mainloop)
|
|
||||||
}
|
|
||||||
|
|
||||||
C.pa_stream_cork(c.stream, 0, C.pa_stream_success_cb_t(C.ebiten_readerdriver_streamSuccessCallback), unsafe.Pointer(c.mainloop))
|
|
||||||
|
|
||||||
return c, ready, nil
|
return c, ready, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (c *context) alsaPcmHwParams(sampleRate, channelNum int, bufferSize, periodSize *C.snd_pcm_uframes_t) error {
|
||||||
|
var params *C.snd_pcm_hw_params_t
|
||||||
|
C.snd_pcm_hw_params_malloc(¶ms)
|
||||||
|
defer C.free(unsafe.Pointer(params))
|
||||||
|
|
||||||
|
if err := C.snd_pcm_hw_params_any(c.handle, params); err < 0 {
|
||||||
|
return alsaError(err)
|
||||||
|
}
|
||||||
|
if err := C.snd_pcm_hw_params_set_access(c.handle, params, C.SND_PCM_ACCESS_RW_INTERLEAVED); err < 0 {
|
||||||
|
return alsaError(err)
|
||||||
|
}
|
||||||
|
if err := C.snd_pcm_hw_params_set_format(c.handle, params, C.SND_PCM_FORMAT_FLOAT_LE); err < 0 {
|
||||||
|
return alsaError(err)
|
||||||
|
}
|
||||||
|
if err := C.snd_pcm_hw_params_set_channels(c.handle, params, C.unsigned(channelNum)); err < 0 {
|
||||||
|
return alsaError(err)
|
||||||
|
}
|
||||||
|
if err := C.snd_pcm_hw_params_set_rate_resample(c.handle, params, 1); err < 0 {
|
||||||
|
return alsaError(err)
|
||||||
|
}
|
||||||
|
sr := C.unsigned(sampleRate)
|
||||||
|
if err := C.snd_pcm_hw_params_set_rate_near(c.handle, params, &sr, nil); err < 0 {
|
||||||
|
return alsaError(err)
|
||||||
|
}
|
||||||
|
if err := C.snd_pcm_hw_params_set_buffer_size_near(c.handle, params, bufferSize); err < 0 {
|
||||||
|
return alsaError(err)
|
||||||
|
}
|
||||||
|
if err := C.snd_pcm_hw_params_set_period_size_near(c.handle, params, periodSize, nil); err < 0 {
|
||||||
|
return alsaError(err)
|
||||||
|
}
|
||||||
|
if err := C.snd_pcm_hw_params(c.handle, params); err < 0 {
|
||||||
|
return alsaError(err)
|
||||||
|
}
|
||||||
|
c.supportsPause = C.snd_pcm_hw_params_can_pause(params) == 1
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *context) readAndWrite(buf32 []float32) error {
|
||||||
|
for i := range buf32 {
|
||||||
|
buf32[i] = 0
|
||||||
|
}
|
||||||
|
c.players.read(buf32)
|
||||||
|
|
||||||
|
for len(buf32) > 0 {
|
||||||
|
n := C.snd_pcm_writei(c.handle, unsafe.Pointer(&buf32[0]), C.snd_pcm_uframes_t(len(buf32)/c.channelNum))
|
||||||
|
if n == -C.EPIPE {
|
||||||
|
// Underrun or overrun occurred.
|
||||||
|
if err := C.snd_pcm_prepare(c.handle); err < 0 {
|
||||||
|
return alsaError(err)
|
||||||
|
}
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if n < 0 {
|
||||||
|
return alsaError(C.int(n))
|
||||||
|
}
|
||||||
|
buf32 = buf32[int(n)*c.channelNum:]
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
func (c *context) Suspend() error {
|
func (c *context) Suspend() error {
|
||||||
C.pa_stream_cork(c.stream, 1, C.pa_stream_success_cb_t(C.ebiten_readerdriver_streamSuccessCallback), unsafe.Pointer(c.mainloop))
|
if c.supportsPause {
|
||||||
|
if err := C.snd_pcm_pause(c.handle, 1); err < 0 {
|
||||||
|
return alsaError(err)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := C.snd_pcm_drop(c.handle); err < 0 {
|
||||||
|
return alsaError(err)
|
||||||
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *context) Resume() error {
|
func (c *context) Resume() error {
|
||||||
C.pa_stream_cork(c.stream, 0, C.pa_stream_success_cb_t(C.ebiten_readerdriver_streamSuccessCallback), unsafe.Pointer(c.mainloop))
|
if c.supportsPause {
|
||||||
|
if err := C.snd_pcm_pause(c.handle, 0); err < 0 {
|
||||||
|
return alsaError(err)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
try:
|
||||||
|
if err := C.snd_pcm_resume(c.handle); err < 0 {
|
||||||
|
if err == -C.EAGAIN {
|
||||||
|
time.Sleep(100 * time.Millisecond)
|
||||||
|
goto try
|
||||||
|
}
|
||||||
|
if err == -C.ENOSYS {
|
||||||
|
if err := C.snd_pcm_prepare(c.handle); err < 0 {
|
||||||
|
return alsaError(err)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return alsaError(err)
|
||||||
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
//export ebiten_readerdriver_contextStateCallback
|
|
||||||
func ebiten_readerdriver_contextStateCallback(context *C.pa_context, mainloop unsafe.Pointer) {
|
|
||||||
C.pa_threaded_mainloop_signal((*C.pa_threaded_mainloop)(mainloop), 0)
|
|
||||||
}
|
|
||||||
|
|
||||||
//export ebiten_readerdriver_streamStateCallback
|
|
||||||
func ebiten_readerdriver_streamStateCallback(stream *C.pa_stream, mainloop unsafe.Pointer) {
|
|
||||||
C.pa_threaded_mainloop_signal((*C.pa_threaded_mainloop)(mainloop), 0)
|
|
||||||
}
|
|
||||||
|
|
||||||
//export ebiten_readerdriver_streamSuccessCallback
|
|
||||||
func ebiten_readerdriver_streamSuccessCallback(stream *C.pa_stream, userdata unsafe.Pointer) {
|
|
||||||
}
|
|
||||||
|
|
||||||
//export ebiten_readerdriver_streamWriteCallback
|
|
||||||
func ebiten_readerdriver_streamWriteCallback(stream *C.pa_stream, requestedBytes C.size_t, userdata unsafe.Pointer) {
|
|
||||||
c := theContext
|
|
||||||
|
|
||||||
var buf unsafe.Pointer
|
|
||||||
var buf32 []float32
|
|
||||||
var bytesToFill C.size_t = bufferSize
|
|
||||||
for n := int(requestedBytes); n > 0; n -= int(bytesToFill) {
|
|
||||||
C.pa_stream_begin_write(stream, &buf, &bytesToFill)
|
|
||||||
if len(buf32) < int(bytesToFill)/4 {
|
|
||||||
buf32 = make([]float32, bytesToFill/4)
|
|
||||||
} else {
|
|
||||||
for i := 0; i < int(bytesToFill)/4; i++ {
|
|
||||||
buf32[i] = 0
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
c.players.read(buf32[:bytesToFill/4])
|
|
||||||
|
|
||||||
for i := uintptr(0); i < uintptr(bytesToFill/4); i++ {
|
|
||||||
*(*float32)(unsafe.Pointer(uintptr(buf) + 4*i)) = buf32[i]
|
|
||||||
}
|
|
||||||
|
|
||||||
C.pa_stream_write(stream, buf, bytesToFill, nil, 0, C.PA_SEEK_RELATIVE)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
Loading…
Reference in New Issue
Block a user