mirror of
https://github.com/hajimehoshi/ebiten.git
synced 2024-11-10 13:07:26 +01:00
17869143c0
Creating a new AudioQueue is more expensive than expected, and this caused a little blocking the game when playing a lot of SEs. Closes #1702 Updates #1680
202 lines
5.3 KiB
Go
202 lines
5.3 KiB
Go
// Copyright 2021 The Ebiten 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.
|
|
|
|
package readerdriver
|
|
|
|
// #cgo LDFLAGS: -framework AudioToolbox
|
|
//
|
|
// #import <AudioToolbox/AudioToolbox.h>
|
|
//
|
|
// void ebiten_readerdriver_render(void* inUserData, AudioQueueRef inAQ, AudioQueueBufferRef inBuffer);
|
|
//
|
|
// void ebiten_readerdriver_setNotificationHandler();
|
|
import "C"
|
|
|
|
import (
|
|
"fmt"
|
|
"sync"
|
|
"time"
|
|
"unsafe"
|
|
)
|
|
|
|
func IsAvailable() bool {
|
|
return true
|
|
}
|
|
|
|
const (
|
|
float32SizeInBytes = 4
|
|
)
|
|
|
|
func newAudioQueue(sampleRate, channelNum, bitDepthInBytes int) (C.AudioQueueRef, []C.AudioQueueBufferRef, error) {
|
|
desc := C.AudioStreamBasicDescription{
|
|
mSampleRate: C.double(sampleRate),
|
|
mFormatID: C.kAudioFormatLinearPCM,
|
|
mFormatFlags: C.kAudioFormatFlagIsFloat,
|
|
mBytesPerPacket: C.UInt32(channelNum * float32SizeInBytes),
|
|
mFramesPerPacket: 1,
|
|
mBytesPerFrame: C.UInt32(channelNum * float32SizeInBytes),
|
|
mChannelsPerFrame: C.UInt32(channelNum),
|
|
mBitsPerChannel: C.UInt32(8 * float32SizeInBytes),
|
|
}
|
|
|
|
var audioQueue C.AudioQueueRef
|
|
if osstatus := C.AudioQueueNewOutput(
|
|
&desc,
|
|
(C.AudioQueueOutputCallback)(C.ebiten_readerdriver_render),
|
|
nil,
|
|
(C.CFRunLoopRef)(0),
|
|
(C.CFStringRef)(0),
|
|
0,
|
|
&audioQueue); osstatus != C.noErr {
|
|
return nil, nil, fmt.Errorf("readerdriver: AudioQueueNewFormat with StreamFormat failed: %d", osstatus)
|
|
}
|
|
|
|
bufs := make([]C.AudioQueueBufferRef, 0, 4)
|
|
for len(bufs) < cap(bufs) {
|
|
var buf C.AudioQueueBufferRef
|
|
if osstatus := C.AudioQueueAllocateBuffer(audioQueue, bufferSizeInBytes, &buf); osstatus != C.noErr {
|
|
return nil, nil, fmt.Errorf("readerdriver: AudioQueueAllocateBuffer failed: %d", osstatus)
|
|
}
|
|
buf.mAudioDataByteSize = bufferSizeInBytes
|
|
bufs = append(bufs, buf)
|
|
}
|
|
|
|
return audioQueue, bufs, nil
|
|
}
|
|
|
|
type context struct {
|
|
sampleRate int
|
|
channelNum int
|
|
bitDepthInBytes int
|
|
|
|
audioQueue C.AudioQueueRef
|
|
unqueuedBuffers []C.AudioQueueBufferRef
|
|
|
|
cond *sync.Cond
|
|
|
|
players *players
|
|
}
|
|
|
|
// TOOD: Convert the error code correctly.
|
|
// See https://stackoverflow.com/questions/2196869/how-do-you-convert-an-iphone-osstatus-code-to-something-useful
|
|
|
|
var theContext *context
|
|
|
|
func NewContext(sampleRate, channelNum, bitDepthInBytes int) (Context, chan struct{}, error) {
|
|
ready := make(chan struct{})
|
|
close(ready)
|
|
|
|
c := &context{
|
|
sampleRate: sampleRate,
|
|
channelNum: channelNum,
|
|
bitDepthInBytes: bitDepthInBytes,
|
|
cond: sync.NewCond(&sync.Mutex{}),
|
|
players: newPlayers(),
|
|
}
|
|
theContext = c
|
|
|
|
q, bs, err := newAudioQueue(sampleRate, channelNum, bitDepthInBytes)
|
|
if err != nil {
|
|
return nil, nil, err
|
|
}
|
|
c.audioQueue = q
|
|
c.unqueuedBuffers = bs
|
|
|
|
C.ebiten_readerdriver_setNotificationHandler()
|
|
|
|
if osstatus := C.AudioQueueStart(c.audioQueue, nil); osstatus != C.noErr {
|
|
return nil, nil, fmt.Errorf("readerdriver: AudioQueueStart failed: %d", osstatus)
|
|
}
|
|
|
|
go c.loop()
|
|
|
|
return c, ready, nil
|
|
}
|
|
|
|
func (c *context) wait() {
|
|
c.cond.L.Lock()
|
|
defer c.cond.L.Unlock()
|
|
|
|
for len(c.unqueuedBuffers) == 0 {
|
|
c.cond.Wait()
|
|
}
|
|
}
|
|
|
|
func (c *context) loop() {
|
|
buf32 := make([]float32, bufferSizeInBytes/4)
|
|
for {
|
|
c.wait()
|
|
c.appendBuffer(buf32)
|
|
}
|
|
}
|
|
|
|
func (c *context) appendBuffer(buf32 []float32) {
|
|
c.cond.L.Lock()
|
|
defer c.cond.L.Unlock()
|
|
|
|
buf := c.unqueuedBuffers[0]
|
|
c.unqueuedBuffers = c.unqueuedBuffers[1:]
|
|
|
|
for i := range buf32 {
|
|
buf32[i] = 0
|
|
}
|
|
c.players.read(buf32)
|
|
for i, f := range buf32 {
|
|
*(*float32)(unsafe.Pointer(uintptr(buf.mAudioData) + uintptr(i)*float32SizeInBytes)) = f
|
|
}
|
|
|
|
if osstatus := C.AudioQueueEnqueueBuffer(c.audioQueue, buf, 0, nil); osstatus != C.noErr {
|
|
// TODO: Treat the error correctly
|
|
panic(fmt.Errorf("readerdriver: AudioQueueEnqueueBuffer failed: %d", osstatus))
|
|
}
|
|
}
|
|
|
|
func (c *context) Suspend() error {
|
|
if osstatus := C.AudioQueuePause(c.audioQueue); osstatus != C.noErr {
|
|
return fmt.Errorf("readerdriver: AudioQueuePause failed: %d", osstatus)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (c *context) Resume() error {
|
|
try:
|
|
if osstatus := C.AudioQueueStart(c.audioQueue, nil); osstatus != C.noErr {
|
|
const AVAudioSessionErrorCodeSiriIsRecording = 0x73697269 // 'siri'
|
|
if osstatus == AVAudioSessionErrorCodeSiriIsRecording {
|
|
time.Sleep(10 * time.Millisecond)
|
|
goto try
|
|
}
|
|
return fmt.Errorf("readerdriver: AudioQueueStart failed: %d", osstatus)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
//export ebiten_readerdriver_render
|
|
func ebiten_readerdriver_render(inUserData unsafe.Pointer, inAQ C.AudioQueueRef, inBuffer C.AudioQueueBufferRef) {
|
|
theContext.cond.L.Lock()
|
|
defer theContext.cond.L.Unlock()
|
|
theContext.unqueuedBuffers = append(theContext.unqueuedBuffers, inBuffer)
|
|
theContext.cond.Signal()
|
|
}
|
|
|
|
//export ebiten_readerdriver_setGlobalPause
|
|
func ebiten_readerdriver_setGlobalPause() {
|
|
theContext.Suspend()
|
|
}
|
|
|
|
//export ebiten_readerdriver_setGlobalResume
|
|
func ebiten_readerdriver_setGlobalResume() {
|
|
theContext.Resume()
|
|
}
|