mirror of
https://github.com/make-42/hayai.git
synced 2025-01-18 18:47:10 +01:00
344 lines
8.4 KiB
Go
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))
|
|
}
|