audio/internal/readerdriver: Use ALSA instead of PulseAudio

Updates #1633
Closes #1761
Updates #1766
This commit is contained in:
Hajime Hoshi 2021-08-17 03:07:21 +09:00
parent 3dcb3d7ea7
commit 13e84f0224
7 changed files with 117 additions and 147 deletions

View File

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

View File

@ -1,8 +1,7 @@
image: archlinux image: archlinux
packages: packages:
- alsa - alsa-lib
- libxcursor - libxcursor
- libpulse
- libxi - libxi
- libxinerama - libxinerama
- libxrandr - libxrandr

View File

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

View File

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

View File

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

View File

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

View File

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