diff --git a/.builds/alpine.yml b/.builds/alpine.yml index cf741be7d..876ac089f 100644 --- a/.builds/alpine.yml +++ b/.builds/alpine.yml @@ -8,7 +8,6 @@ packages: - libxrandr-dev - mesa-dev - pkgconf - - pulseaudio-dev - go sources: - https://github.com/hajimehoshi/ebiten diff --git a/.builds/arch.yml b/.builds/arch.yml index c4c11352f..6b620e265 100644 --- a/.builds/arch.yml +++ b/.builds/arch.yml @@ -1,8 +1,7 @@ image: archlinux packages: - - alsa + - alsa-lib - libxcursor - - libpulse - libxi - libxinerama - libxrandr diff --git a/.builds/debian.yml b/.builds/debian.yml index d0dbe499f..2f43d8274 100644 --- a/.builds/debian.yml +++ b/.builds/debian.yml @@ -4,7 +4,6 @@ packages: - libc6-dev - libglu1-mesa-dev - libgl1-mesa-dev - - libpulse-dev - libxcursor-dev - libxi-dev - libxinerama-dev diff --git a/.builds/fedora.yml b/.builds/fedora.yml index 46c1d7744..296ac5558 100644 --- a/.builds/fedora.yml +++ b/.builds/fedora.yml @@ -9,7 +9,6 @@ packages: - mesa-libGLES-devel - mesa-libGLU-devel - pkg-config - - pulseaudio-libs-devel - go sources: - https://github.com/hajimehoshi/ebiten diff --git a/.builds/ubuntu.yml b/.builds/ubuntu.yml index a4d6343ba..4c5e60bd0 100644 --- a/.builds/ubuntu.yml +++ b/.builds/ubuntu.yml @@ -4,7 +4,6 @@ packages: - libc6-dev - libglu1-mesa-dev - libgl1-mesa-dev - - libpulse-dev - libxcursor-dev - libxi-dev - libxinerama-dev diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 3640c9855..57eb69ac2 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 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 run: | diff --git a/audio/internal/readerdriver/driver_unix.go b/audio/internal/readerdriver/driver_unix.go index f4697b1e1..a4a9a8e86 100644 --- a/audio/internal/readerdriver/driver_unix.go +++ b/audio/internal/readerdriver/driver_unix.go @@ -17,20 +17,14 @@ package readerdriver -// #cgo pkg-config: libpulse -// #cgo LDFLAGS: -lpulse +// #cgo pkg-config: alsa // -// #include -// -// 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); +// #include import "C" import ( "fmt" - "runtime" + "time" "unsafe" ) @@ -43,16 +37,17 @@ type context struct { channelNum int bitDepthInBytes int - mainloop *C.pa_threaded_mainloop - context *C.pa_context - stream *C.pa_stream + handle *C.snd_pcm_t + supportsPause bool players *players } 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) { ready := make(chan struct{}) @@ -66,146 +61,126 @@ func NewContext(sampleRate, channelNum, bitDepthInBytes int) (Context, chan stru } theContext = c - c.mainloop = C.pa_threaded_mainloop_new() - if c.mainloop == nil { - return nil, nil, fmt.Errorf("readerdriver: pa_threaded_mainloop_new failed") - } - mainloopAPI := C.pa_threaded_mainloop_get_api(c.mainloop) - if mainloopAPI == nil { - return nil, nil, fmt.Errorf("readerdriver: pa_threaded_mainloop_get_api failed") + // Open a default ALSA audio device for blocking stream playback + cname := C.CString("default") + defer C.free(unsafe.Pointer(cname)) + if err := C.snd_pcm_open(&c.handle, cname, C.SND_PCM_STREAM_PLAYBACK, 0); err < 0 { + return nil, nil, alsaError(err) } - contextName := C.CString("pcm-playback") - defer C.free(unsafe.Pointer(contextName)) - c.context = C.pa_context_new(mainloopAPI, contextName) - if c.context == nil { - return nil, nil, fmt.Errorf("readerdriver: pa_context_new failed") + periodSize := C.snd_pcm_uframes_t(1024) + bufferSize := periodSize * 2 + if err := c.alsaPcmHwParams(sampleRate, channelNum, &bufferSize, &periodSize); err != nil { + return nil, nil, err } - C.pa_context_set_state_callback(c.context, C.pa_context_notify_cb_t(C.ebiten_readerdriver_contextStateCallback), unsafe.Pointer(c.mainloop)) - - runtime.LockOSThread() - defer runtime.UnlockOSThread() - C.pa_threaded_mainloop_lock(c.mainloop) - 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") + go func() { + buf32 := make([]float32, int(periodSize)*c.channelNum) + for { + if err := c.readAndWrite(buf32); err != nil { + panic(err) + } } - 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 } +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 { - 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 } 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 } - -//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) - } -}