subsonic-tui/vendor/github.com/ebitengine/oto/v3/driver_unix.go
Sagi Dayan 26e526f07e
initial commit
Signed-off-by: Sagi Dayan <sagidayan@gmail.com>
2024-02-20 19:10:14 +02:00

278 lines
7 KiB
Go

// Copyright 2021 The Oto 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.
//go:build !android && !darwin && !js && !windows && !nintendosdk
package oto
// #cgo pkg-config: alsa
//
// #include <alsa/asoundlib.h>
import "C"
import (
"fmt"
"strings"
"sync"
"unsafe"
"github.com/ebitengine/oto/v3/internal/mux"
)
type context struct {
channelCount int
suspended bool
handle *C.snd_pcm_t
cond *sync.Cond
mux *mux.Mux
err atomicError
ready chan struct{}
}
var theContext *context
func alsaError(name string, err C.int) error {
return fmt.Errorf("oto: ALSA error at %s: %s", name, C.GoString(C.snd_strerror(err)))
}
func deviceCandidates() []string {
const getAllDevices = -1
cPCMInterfaceName := C.CString("pcm")
defer C.free(unsafe.Pointer(cPCMInterfaceName))
var hints *unsafe.Pointer
err := C.snd_device_name_hint(getAllDevices, cPCMInterfaceName, &hints)
if err != 0 {
return []string{"default", "plug:default"}
}
defer C.snd_device_name_free_hint(hints)
var devices []string
cIoHintName := C.CString("IOID")
defer C.free(unsafe.Pointer(cIoHintName))
cNameHintName := C.CString("NAME")
defer C.free(unsafe.Pointer(cNameHintName))
for it := hints; *it != nil; it = (*unsafe.Pointer)(unsafe.Pointer(uintptr(unsafe.Pointer(it)) + unsafe.Sizeof(uintptr(0)))) {
io := C.snd_device_name_get_hint(*it, cIoHintName)
defer func() {
if io != nil {
C.free(unsafe.Pointer(io))
}
}()
if C.GoString(io) == "Input" {
continue
}
name := C.snd_device_name_get_hint(*it, cNameHintName)
defer func() {
if name != nil {
C.free(unsafe.Pointer(name))
}
}()
if name == nil {
continue
}
goName := C.GoString(name)
if goName == "null" {
continue
}
if goName == "default" {
continue
}
devices = append(devices, goName)
}
devices = append([]string{"default", "plug:default"}, devices...)
return devices
}
func newContext(sampleRate int, channelCount int, format mux.Format, bufferSizeInBytes int) (*context, chan struct{}, error) {
c := &context{
channelCount: channelCount,
cond: sync.NewCond(&sync.Mutex{}),
mux: mux.New(sampleRate, channelCount, format),
ready: make(chan struct{}),
}
theContext = c
go func() {
defer close(c.ready)
// Open a default ALSA audio device for blocking stream playback
type openError struct {
device string
err C.int
}
var openErrs []openError
var found bool
for _, name := range deviceCandidates() {
cname := C.CString(name)
defer C.free(unsafe.Pointer(cname))
if err := C.snd_pcm_open(&c.handle, cname, C.SND_PCM_STREAM_PLAYBACK, 0); err < 0 {
openErrs = append(openErrs, openError{
device: name,
err: err,
})
continue
}
found = true
break
}
if !found {
var msgs []string
for _, e := range openErrs {
msgs = append(msgs, fmt.Sprintf("%q: %s", e.device, C.GoString(C.snd_strerror(e.err))))
}
c.err.TryStore(fmt.Errorf("oto: ALSA error at snd_pcm_open: %s", strings.Join(msgs, ", ")))
return
}
// TODO: Should snd_pcm_hw_params_set_periods be called explicitly?
const periods = 2
var periodSize C.snd_pcm_uframes_t
if bufferSizeInBytes != 0 {
periodSize = C.snd_pcm_uframes_t(bufferSizeInBytes / (channelCount * 4 * periods))
} else {
periodSize = C.snd_pcm_uframes_t(1024)
}
bufferSize := periodSize * periods
if err := c.alsaPcmHwParams(sampleRate, channelCount, &bufferSize, &periodSize); err != nil {
c.err.TryStore(err)
return
}
go func() {
buf32 := make([]float32, int(periodSize)*channelCount)
for {
if !c.readAndWrite(buf32) {
return
}
}
}()
}()
return c, c.ready, nil
}
func (c *context) alsaPcmHwParams(sampleRate, channelCount 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("snd_pcm_hw_params_any", err)
}
if err := C.snd_pcm_hw_params_set_access(c.handle, params, C.SND_PCM_ACCESS_RW_INTERLEAVED); err < 0 {
return alsaError("snd_pcm_hw_params_set_access", err)
}
if err := C.snd_pcm_hw_params_set_format(c.handle, params, C.SND_PCM_FORMAT_FLOAT_LE); err < 0 {
return alsaError("snd_pcm_hw_params_set_format", err)
}
if err := C.snd_pcm_hw_params_set_channels(c.handle, params, C.unsigned(channelCount)); err < 0 {
return alsaError("snd_pcm_hw_params_set_channels", err)
}
if err := C.snd_pcm_hw_params_set_rate_resample(c.handle, params, 1); err < 0 {
return alsaError("snd_pcm_hw_params_set_rate_resample", err)
}
sr := C.unsigned(sampleRate)
if err := C.snd_pcm_hw_params_set_rate_near(c.handle, params, &sr, nil); err < 0 {
return alsaError("snd_pcm_hw_params_set_rate_near", err)
}
if err := C.snd_pcm_hw_params_set_buffer_size_near(c.handle, params, bufferSize); err < 0 {
return alsaError("snd_pcm_hw_params_set_buffer_size_near", err)
}
if err := C.snd_pcm_hw_params_set_period_size_near(c.handle, params, periodSize, nil); err < 0 {
return alsaError("snd_pcm_hw_params_set_period_size_near", err)
}
if err := C.snd_pcm_hw_params(c.handle, params); err < 0 {
return alsaError("snd_pcm_hw_params", err)
}
return nil
}
func (c *context) readAndWrite(buf32 []float32) bool {
c.cond.L.Lock()
defer c.cond.L.Unlock()
for c.suspended && c.err.Load() == nil {
c.cond.Wait()
}
if c.err.Load() != nil {
return false
}
c.mux.ReadFloat32s(buf32)
for len(buf32) > 0 {
n := C.snd_pcm_writei(c.handle, unsafe.Pointer(&buf32[0]), C.snd_pcm_uframes_t(len(buf32)/c.channelCount))
if n < 0 {
n = C.long(C.snd_pcm_recover(c.handle, C.int(n), 1))
}
if n < 0 {
c.err.TryStore(alsaError("snd_pcm_writei or snd_pcm_recover", C.int(n)))
return false
}
buf32 = buf32[int(n)*c.channelCount:]
}
return true
}
func (c *context) Suspend() error {
<-c.ready
c.cond.L.Lock()
defer c.cond.L.Unlock()
if err := c.err.Load(); err != nil {
return err.(error)
}
c.suspended = true
// Do not use snd_pcm_pause as not all devices support this.
// Do not use snd_pcm_drop as this might hang (https://github.com/libsdl-org/SDL/blob/a5c610b0a3857d3138f3f3da1f6dc3172c5ea4a8/src/audio/alsa/SDL_alsa_audio.c#L478).
return nil
}
func (c *context) Resume() error {
<-c.ready
c.cond.L.Lock()
defer c.cond.L.Unlock()
if err := c.err.Load(); err != nil {
return err.(error)
}
c.suspended = false
c.cond.Signal()
return nil
}
func (c *context) Err() error {
if err := c.err.Load(); err != nil {
return err.(error)
}
return nil
}