hayai/vendor/github.com/hajimehoshi/oto/driver_darwin.go
2024-12-21 17:26:50 +01:00

344 lines
8.4 KiB
Go

// Copyright 2019 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.
// +build !js
package oto
// #cgo LDFLAGS: -framework AudioToolbox
//
// #import <AudioToolbox/AudioToolbox.h>
//
// void oto_render(void* inUserData, AudioQueueRef inAQ, AudioQueueBufferRef inBuffer);
//
// void oto_setNotificationHandler();
// bool oto_isBackground(void);
import "C"
import (
"fmt"
"runtime"
"sync"
"time"
"unsafe"
)
const baseQueueBufferSize = 1024
type audioInfo struct {
channelNum int
bitDepthInBytes int
}
type driver struct {
audioQueue C.AudioQueueRef
buf []byte
bufSize int
sampleRate int
audioInfo *audioInfo
buffers []C.AudioQueueBufferRef
paused bool
lastPauseTime time.Time
err error
chWrite chan []byte
chWritten chan int
m sync.Mutex
}
var (
theDriver *driver
driverM sync.Mutex
)
func setDriver(d *driver) {
driverM.Lock()
defer driverM.Unlock()
if theDriver != nil && d != nil {
panic("oto: at most one driver object can exist")
}
theDriver = d
if d != nil {
setNotificationHandler(d)
}
}
func getDriver() *driver {
driverM.Lock()
defer driverM.Unlock()
return theDriver
}
// TOOD: Convert the error code correctly.
// See https://stackoverflow.com/questions/2196869/how-do-you-convert-an-iphone-osstatus-code-to-something-useful
func newDriver(sampleRate, channelNum, bitDepthInBytes, bufferSizeInBytes int) (tryWriteCloser, error) {
flags := C.kAudioFormatFlagIsPacked
if bitDepthInBytes != 1 {
flags |= C.kAudioFormatFlagIsSignedInteger
}
desc := C.AudioStreamBasicDescription{
mSampleRate: C.double(sampleRate),
mFormatID: C.kAudioFormatLinearPCM,
mFormatFlags: C.UInt32(flags),
mBytesPerPacket: C.UInt32(channelNum * bitDepthInBytes),
mFramesPerPacket: 1,
mBytesPerFrame: C.UInt32(channelNum * bitDepthInBytes),
mChannelsPerFrame: C.UInt32(channelNum),
mBitsPerChannel: C.UInt32(8 * bitDepthInBytes),
}
audioInfo := &audioInfo{
channelNum: channelNum,
bitDepthInBytes: bitDepthInBytes,
}
var audioQueue C.AudioQueueRef
if osstatus := C.AudioQueueNewOutput(
&desc,
(C.AudioQueueOutputCallback)(C.oto_render),
unsafe.Pointer(audioInfo),
(C.CFRunLoopRef)(0),
(C.CFStringRef)(0),
0,
&audioQueue); osstatus != C.noErr {
return nil, fmt.Errorf("oto: AudioQueueNewFormat with StreamFormat failed: %d", osstatus)
}
queueBufferSize := baseQueueBufferSize * channelNum * bitDepthInBytes
nbuf := bufferSizeInBytes / queueBufferSize
if nbuf <= 1 {
nbuf = 2
}
d := &driver{
audioQueue: audioQueue,
sampleRate: sampleRate,
audioInfo: audioInfo,
bufSize: nbuf * queueBufferSize,
buffers: make([]C.AudioQueueBufferRef, nbuf),
chWrite: make(chan []byte),
chWritten: make(chan int),
}
runtime.SetFinalizer(d, (*driver).Close)
// Set the driver before setting the rendering callback.
setDriver(d)
for i := 0; i < len(d.buffers); i++ {
var buf C.AudioQueueBufferRef
if osstatus := C.AudioQueueAllocateBuffer(audioQueue, C.UInt32(queueBufferSize), &buf); osstatus != C.noErr {
return nil, fmt.Errorf("oto: AudioQueueAllocateBuffer failed: %d", osstatus)
}
d.buffers[i] = buf
d.buffers[i].mAudioDataByteSize = C.UInt32(queueBufferSize)
for j := 0; j < queueBufferSize; j++ {
*(*byte)(unsafe.Pointer(uintptr(unsafe.Pointer(d.buffers[i].mAudioData)) + uintptr(j))) = 0
}
if osstatus := C.AudioQueueEnqueueBuffer(audioQueue, d.buffers[i], 0, nil); osstatus != C.noErr {
return nil, fmt.Errorf("oto: AudioQueueEnqueueBuffer failed: %d", osstatus)
}
}
for C.oto_isBackground() {
time.Sleep(time.Second)
}
if osstatus := C.AudioQueueStart(audioQueue, nil); osstatus != C.noErr {
return nil, fmt.Errorf("oto: AudioQueueStart failed: %d", osstatus)
}
return d, nil
}
//export oto_render
func oto_render(inUserData unsafe.Pointer, inAQ C.AudioQueueRef, inBuffer C.AudioQueueBufferRef) {
audioInfo := (*audioInfo)(inUserData)
queueBufferSize := baseQueueBufferSize * audioInfo.channelNum * audioInfo.bitDepthInBytes
d := getDriver()
var buf []byte
// Set the timer. When the input does not come, the audio must be paused.
s := time.Second * time.Duration(queueBufferSize) / time.Duration(d.sampleRate*d.audioInfo.channelNum*d.audioInfo.bitDepthInBytes)
t := time.NewTicker(s)
defer t.Stop()
ch := t.C
for len(buf) < queueBufferSize {
select {
case dbuf := <-d.chWrite:
for !d.resume(false) {
d.m.Lock()
err := d.err
d.m.Unlock()
if err != nil {
return
}
time.Sleep(time.Second)
}
n := queueBufferSize - len(buf)
if n > len(dbuf) {
n = len(dbuf)
}
buf = append(buf, dbuf[:n]...)
d.chWritten <- n
case <-ch:
d.pause()
ch = nil
}
}
for i := 0; i < queueBufferSize; i++ {
*(*byte)(unsafe.Pointer(uintptr(inBuffer.mAudioData) + uintptr(i))) = buf[i]
}
// Do not update mAudioDataByteSize, or the buffer is not used correctly any more.
d.enqueueBuffer(inBuffer)
}
func (d *driver) TryWrite(data []byte) (int, error) {
d.m.Lock()
err := d.err
d.m.Unlock()
if err != nil {
return 0, err
}
n := d.bufSize - len(d.buf)
if n > len(data) {
n = len(data)
}
d.buf = append(d.buf, data[:n]...)
// Use the buffer only when the buffer length is enough to avoid choppy sound.
queueBufferSize := baseQueueBufferSize * d.audioInfo.channelNum * d.audioInfo.bitDepthInBytes
for len(d.buf) >= queueBufferSize {
d.chWrite <- d.buf
n := <-d.chWritten
d.buf = d.buf[n:]
}
return n, nil
}
func (d *driver) Close() error {
d.m.Lock()
defer d.m.Unlock()
runtime.SetFinalizer(d, nil)
if osstatus := C.AudioQueueStop(d.audioQueue, C.false); osstatus != C.noErr {
return fmt.Errorf("oto: AudioQueueStop failed: %d", osstatus)
}
if osstatus := C.AudioQueueDispose(d.audioQueue, C.false); osstatus != C.noErr {
return fmt.Errorf("oto: AudioQueueDispose failed: %d", osstatus)
}
d.audioQueue = nil
setDriver(nil)
return nil
}
func (d *driver) enqueueBuffer(buffer C.AudioQueueBufferRef) {
d.m.Lock()
defer d.m.Unlock()
if osstatus := C.AudioQueueEnqueueBuffer(d.audioQueue, buffer, 0, nil); osstatus != C.noErr && d.err == nil {
d.err = fmt.Errorf("oto: AudioQueueEnqueueBuffer failed: %d", osstatus)
return
}
}
func (d *driver) resume(afterSleep bool) bool {
d.m.Lock()
defer d.m.Unlock()
// Audio doesn't work soon after recovering from sleeping. Wait for a while
// (hajimehoshi/ebiten#1259).
if afterSleep {
// After short-time sleeping, 500ms more sleeping is enough. However, after long-time sleeping, it
// looks like 1 second more sleeping are required (hajimehoshi/ebiten#1280).
// This is tested on MacBook Pro 2020 macOS 10.15.6.
if time.Now().Sub(d.lastPauseTime) < 30*time.Second {
time.Sleep(500 * time.Millisecond)
} else {
time.Sleep(time.Second)
}
}
if C.oto_isBackground() {
return false
}
if osstatus := C.AudioQueueStart(d.audioQueue, nil); osstatus != C.noErr && d.err == nil {
d.err = fmt.Errorf("oto: AudioQueueStart for resuming failed: %d", osstatus)
return false
}
d.paused = false
return true
}
func (d *driver) pause() {
d.m.Lock()
defer d.m.Unlock()
if d.paused {
return
}
if osstatus := C.AudioQueuePause(d.audioQueue); osstatus != C.noErr && d.err == nil {
d.err = fmt.Errorf("oto: AudioQueuePause failed: %d", osstatus)
return
}
d.paused = true
d.lastPauseTime = time.Now()
}
func (d *driver) setError(err error) {
d.m.Lock()
defer d.m.Unlock()
if theDriver.err != nil {
return
}
theDriver.err = err
}
func (d *driver) tryWriteCanReturnWithoutWaiting() bool {
return true
}
func setNotificationHandler(driver *driver) {
C.oto_setNotificationHandler()
}
//export oto_setGlobalPause
func oto_setGlobalPause() {
theDriver.pause()
}
//export oto_setGlobalResume
func oto_setGlobalResume() {
theDriver.resume(true)
}
//export oto_setErrorByNotification
func oto_setErrorByNotification(s C.OSStatus, from *C.char) {
gofrom := C.GoString(from)
theDriver.setError(fmt.Errorf("oto: %s at notification failed: %d", gofrom, s))
}