diff --git a/api_audio_darwin.go b/api_audio_darwin.go deleted file mode 100644 index 63e68bc..0000000 --- a/api_audio_darwin.go +++ /dev/null @@ -1,117 +0,0 @@ -// Copyright 2022 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. - -package oto - -import ( - _ "runtime/cgo" // TODO: remove once purego(#12) is solved. - "unsafe" - - "github.com/ebitengine/purego" -) - -const ( - avAudioSessionErrorCodeCannotStartPlaying = 0x21706c61 // '!pla' - avAudioSessionErrorCodeSiriIsRecording = 0x73697269 // 'siri' -) - -const ( - kAudioFormatLinearPCM = 0x6C70636D //'lpcm' -) - -const ( - kAudioFormatFlagIsFloat = 1 << 0 // 0x1 -) - -type _AudioStreamBasicDescription struct { - mSampleRate float64 - mFormatID uint32 - mFormatFlags uint32 - mBytesPerPacket uint32 - mFramesPerPacket uint32 - mBytesPerFrame uint32 - mChannelsPerFrame uint32 - mBitsPerChannel uint32 - mReserved uint32 -} - -type _AudioQueueRef uintptr - -type _AudioTimeStamp uintptr - -type _AudioStreamPacketDescription struct { - mStartOffset int64 - mVariableFramesInPacket uint32 - mDataByteSize uint32 -} - -type _AudioQueueBufferRef *_AudioQueueBuffer - -type _AudioQueueBuffer struct { - mAudioDataBytesCapacity uint32 - mAudioData uintptr // void* - mAudioDataByteSize uint32 - mUserData uintptr // void* - - mPacketDescriptionCapacity uint32 - mPacketDescriptions *_AudioStreamPacketDescription - mPacketDescriptionCount uint32 -} - -type _AudioQueueOutputCallback func(inUserData unsafe.Pointer, inAQ _AudioQueueRef, inBuffer _AudioQueueBufferRef) - -var ( - toolbox = purego.Dlopen("/System/Library/Frameworks/AudioToolbox.framework/AudioToolbox", purego.RTLD_GLOBAL) - atAudioQueueNewOutput = purego.Dlsym(toolbox, "AudioQueueNewOutput") - atAudioQueueStart = purego.Dlsym(toolbox, "AudioQueueStart") - atAudioQueuePause = purego.Dlsym(toolbox, "AudioQueuePause") - atAudioQueueAllocateBuffer = purego.Dlsym(toolbox, "AudioQueueAllocateBuffer") - atAudioQueueEnqueueBuffer = purego.Dlsym(toolbox, "AudioQueueEnqueueBuffer") -) - -func _AudioQueueNewOutput(inFormat *_AudioStreamBasicDescription, inCallbackProc _AudioQueueOutputCallback, inUserData unsafe.Pointer, inCallbackRunLoop uintptr, inCallbackRunLoopMod uintptr, inFlags uint32, outAQ *_AudioQueueRef) uintptr { - ret, _, _ := purego.SyscallN(atAudioQueueNewOutput, - uintptr(unsafe.Pointer(inFormat)), - purego.NewCallback(inCallbackProc), - uintptr(inUserData), - inCallbackRunLoop, // CFRunLoopRef - inCallbackRunLoopMod, // CFStringRef - uintptr(inFlags), - uintptr(unsafe.Pointer(outAQ))) - return ret -} - -func _AudioQueueAllocateBuffer(inAQ _AudioQueueRef, inBufferByteSize uint32, outBuffer *_AudioQueueBufferRef) uintptr { - ret, _, _ := purego.SyscallN(atAudioQueueAllocateBuffer, uintptr(inAQ), uintptr(inBufferByteSize), uintptr(unsafe.Pointer(outBuffer))) - return ret -} - -func _AudioQueueStart(inAQ _AudioQueueRef, inStartTime *_AudioTimeStamp) uintptr { - ret, _, _ := purego.SyscallN(atAudioQueueStart, uintptr(inAQ), uintptr(unsafe.Pointer(inStartTime))) - return ret -} - -func _AudioQueueEnqueueBuffer(inAQ _AudioQueueRef, inBuffer _AudioQueueBufferRef, inNumPacketDescs uint32, inPackets []_AudioStreamPacketDescription) uintptr { - var packetPtr *_AudioStreamPacketDescription - if len(inPackets) > 0 { - packetPtr = &inPackets[0] - } - ret, _, _ := purego.SyscallN(atAudioQueueEnqueueBuffer, uintptr(inAQ), uintptr(unsafe.Pointer(inBuffer)), uintptr(inNumPacketDescs), uintptr(unsafe.Pointer(packetPtr))) - return ret -} - -func _AudioQueuePause(inAQ _AudioQueueRef) uintptr { - ret, _, _ := purego.SyscallN(atAudioQueuePause, uintptr(inAQ)) - return ret -} diff --git a/driver_darwin.go b/driver_darwin.go index 8dc9561..8c88a0e 100644 --- a/driver_darwin.go +++ b/driver_darwin.go @@ -14,52 +14,62 @@ package oto +// #cgo LDFLAGS: -framework AudioToolbox +// +// #import +// +// void oto_render(void* inUserData, AudioQueueRef inAQ, AudioQueueBufferRef inBuffer); +// +// void oto_setNotificationHandler(); +import "C" + import ( "fmt" "sync" "time" "unsafe" - "github.com/ebitengine/purego/objc" - "github.com/hajimehoshi/oto/v2/internal/mux" ) const ( float32SizeInBytes = 4 +) - noErr = 0 +const ( + avAudioSessionErrorCodeCannotStartPlaying = 0x21706c61 // '!pla' + avAudioSessionErrorCodeSiriIsRecording = 0x73697269 // 'siri' ) -func newAudioQueue(sampleRate, channelCount, bitDepthInBytes int) (_AudioQueueRef, []_AudioQueueBufferRef, error) { - desc := _AudioStreamBasicDescription{ - mSampleRate: float64(sampleRate), - mFormatID: uint32(kAudioFormatLinearPCM), - mFormatFlags: uint32(kAudioFormatFlagIsFloat), - mBytesPerPacket: uint32(channelCount * float32SizeInBytes), +func newAudioQueue(sampleRate, channelCount, bitDepthInBytes int) (C.AudioQueueRef, []C.AudioQueueBufferRef, error) { + desc := C.AudioStreamBasicDescription{ + mSampleRate: C.double(sampleRate), + mFormatID: C.kAudioFormatLinearPCM, + mFormatFlags: C.kAudioFormatFlagIsFloat, + mBytesPerPacket: C.UInt32(channelCount * float32SizeInBytes), mFramesPerPacket: 1, - mBytesPerFrame: uint32(channelCount * float32SizeInBytes), - mChannelsPerFrame: uint32(channelCount), - mBitsPerChannel: uint32(8 * float32SizeInBytes), + mBytesPerFrame: C.UInt32(channelCount * float32SizeInBytes), + mChannelsPerFrame: C.UInt32(channelCount), + mBitsPerChannel: C.UInt32(8 * float32SizeInBytes), } - var audioQueue _AudioQueueRef - if osstatus := _AudioQueueNewOutput( + var audioQueue C.AudioQueueRef + if osstatus := C.AudioQueueNewOutput( &desc, - oto_render, + (C.AudioQueueOutputCallback)(C.oto_render), nil, - 0, //CFRunLoopRef - 0, //CFStringRef + (C.CFRunLoopRef)(0), + (C.CFStringRef)(0), 0, - &audioQueue); osstatus != noErr { - return 0, nil, fmt.Errorf("oto: AudioQueueNewFormat with StreamFormat failed: %d", osstatus) + &audioQueue); osstatus != C.noErr { + return nil, nil, fmt.Errorf("oto: AudioQueueNewFormat with StreamFormat failed: %d", osstatus) } - bufs := make([]_AudioQueueBufferRef, 0, 4) + bufs := make([]C.AudioQueueBufferRef, 0, 4) for len(bufs) < cap(bufs) { - var buf _AudioQueueBufferRef - if osstatus := _AudioQueueAllocateBuffer(audioQueue, bufferSizeInBytes, &buf); osstatus != noErr { - return 0, nil, fmt.Errorf("oto: AudioQueueAllocateBuffer failed: %d", osstatus) + var buf C.AudioQueueBufferRef + if osstatus := C.AudioQueueAllocateBuffer(audioQueue, bufferSizeInBytes, &buf); osstatus != C.noErr { + return nil, nil, fmt.Errorf("oto: AudioQueueAllocateBuffer failed: %d", osstatus) } buf.mAudioDataByteSize = bufferSizeInBytes bufs = append(bufs, buf) @@ -69,8 +79,8 @@ func newAudioQueue(sampleRate, channelCount, bitDepthInBytes int) (_AudioQueueRe } type context struct { - audioQueue _AudioQueueRef - unqueuedBuffers []_AudioQueueBufferRef + audioQueue C.AudioQueueRef + unqueuedBuffers []C.AudioQueueBufferRef cond *sync.Cond @@ -78,7 +88,7 @@ type context struct { err atomicError } -// TODO: Convert the error code correctly. +// 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 @@ -100,11 +110,11 @@ func newContext(sampleRate, channelCount, bitDepthInBytes int) (*context, chan s c.audioQueue = q c.unqueuedBuffers = bs - setNotificationHandler() + C.oto_setNotificationHandler() var retryCount int try: - if osstatus := _AudioQueueStart(c.audioQueue, nil); osstatus != noErr { + if osstatus := C.AudioQueueStart(c.audioQueue, nil); osstatus != C.noErr { if osstatus == avAudioSessionErrorCodeCannotStartPlaying && retryCount < 100 { time.Sleep(10 * time.Millisecond) retryCount++ @@ -152,10 +162,10 @@ func (c *context) appendBuffer(buf32 []float32) { c.mux.ReadFloat32s(buf32) for i, f := range buf32 { - *(*float32)(unsafe.Pointer(buf.mAudioData + uintptr(i)*float32SizeInBytes)) = f + *(*float32)(unsafe.Pointer(uintptr(buf.mAudioData) + uintptr(i)*float32SizeInBytes)) = f } - if osstatus := _AudioQueueEnqueueBuffer(c.audioQueue, buf, 0, nil); osstatus != noErr { + if osstatus := C.AudioQueueEnqueueBuffer(c.audioQueue, buf, 0, nil); osstatus != C.noErr { c.err.TryStore(fmt.Errorf("oto: AudioQueueEnqueueBuffer failed: %d", osstatus)) } } @@ -167,7 +177,8 @@ func (c *context) Suspend() error { if err := c.err.Load(); err != nil { return err.(error) } - if osstatus := _AudioQueuePause(c.audioQueue); osstatus != noErr { + + if osstatus := C.AudioQueuePause(c.audioQueue); osstatus != C.noErr { return fmt.Errorf("oto: AudioQueuePause failed: %d", osstatus) } return nil @@ -183,7 +194,7 @@ func (c *context) Resume() error { var retryCount int try: - if osstatus := _AudioQueueStart(c.audioQueue, nil); osstatus != noErr { + if osstatus := C.AudioQueueStart(c.audioQueue, nil); osstatus != C.noErr { if osstatus == avAudioSessionErrorCodeCannotStartPlaying && retryCount < 100 { time.Sleep(10 * time.Millisecond) retryCount++ @@ -205,17 +216,20 @@ func (c *context) Err() error { return nil } -func oto_render(inUserData unsafe.Pointer, inAQ _AudioQueueRef, inBuffer _AudioQueueBufferRef) { +//export oto_render +func oto_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() } -func oto_setGlobalPause(self objc.ID, _cmd objc.SEL, notification objc.ID) { +//export oto_setGlobalPause +func oto_setGlobalPause() { theContext.Suspend() } -func oto_setGlobalResume(self objc.ID, _cmd objc.SEL, notification objc.ID) { +//export oto_setGlobalResume +func oto_setGlobalResume() { theContext.Resume() } diff --git a/driver_ios.go b/driver_ios.go index 29e1115..213ea9f 100644 --- a/driver_ios.go +++ b/driver_ios.go @@ -22,9 +22,3 @@ package oto // '4' is float32 size in bytes. '2' is a number of channels for stereo. const bufferSizeInBytes = 12288 - -func setNotificationHandler() { - // AVAudioSessionInterruptionNotification is not reliable on iOS. Rely on - // applicationWillResignActive and applicationDidBecomeActive instead. See - // https://stackoverflow.com/questions/24404463/ios-siri-not-available-does-not-return-avaudiosessioninterruptionoptionshouldre -} diff --git a/driver_ios.m b/driver_ios.m new file mode 100644 index 0000000..8a0aa15 --- /dev/null +++ b/driver_ios.m @@ -0,0 +1,23 @@ +// 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 ios +// +build ios + +void oto_setNotificationHandler() { + // AVAudioSessionInterruptionNotification is not reliable on iOS. Rely on + // applicationWillResignActive and applicationDidBecomeActive instead. See + // https://stackoverflow.com/questions/24404463/ios-siri-not-available-does-not-return-avaudiosessioninterruptionoptionshouldre + return; +} diff --git a/driver_macos.go b/driver_macos.go index 6335255..9a70e9c 100644 --- a/driver_macos.go +++ b/driver_macos.go @@ -17,40 +17,7 @@ package oto -import ( - "unsafe" - - "github.com/ebitengine/purego" - "github.com/ebitengine/purego/objc" -) +// #cgo LDFLAGS: -framework AppKit +import "C" const bufferSizeInBytes = 2048 - -var appkit = purego.Dlopen("/System/Library/Frameworks/AppKit.framework/Versions/Current/AppKit", purego.RTLD_GLOBAL) - -// setNotificationHandler sets a handler for sleep/wake notifications. -func setNotificationHandler() { - // Create the Observer object - class := objc.AllocateClassPair(objc.GetClass("NSObject\x00"), "OtoNotificationObserver\x00", 0) - class.AddMethod(objc.RegisterName("receiveSleepNote:\x00"), objc.IMP(oto_setGlobalPause), "v@:@\x00") - class.AddMethod(objc.RegisterName("receiveWakeNote:\x00"), objc.IMP(oto_setGlobalResume), "v@:@\x00") - class.Register() - - observer := objc.ID(class).Send(objc.RegisterName("new\x00")) - - notificationCenter := objc.ID(objc.GetClass("NSWorkspace\x00")).Send(objc.RegisterName("sharedWorkspace\x00")).Send(objc.RegisterName("notificationCenter\x00")) - notificationCenter.Send(objc.RegisterName("addObserver:selector:name:object:\x00"), - observer, - objc.RegisterName("receiveSleepNote:\x00"), - // Dlsym returns a pointer to the object so dereference it - *(*uintptr)(unsafe.Pointer(purego.Dlsym(appkit, "NSWorkspaceWillSleepNotification"))), - 0, - ) - notificationCenter.Send(objc.RegisterName("addObserver:selector:name:object:\x00"), - observer, - objc.RegisterName("receiveWakeNote:\x00"), - // Dlsym returns a pointer to the object so dereference it - *(*uintptr)(unsafe.Pointer(purego.Dlsym(appkit, "NSWorkspaceDidWakeNotification"))), - 0, - ) -} diff --git a/driver_macos.m b/driver_macos.m new file mode 100644 index 0000000..828b571 --- /dev/null +++ b/driver_macos.m @@ -0,0 +1,55 @@ +// Copyright 2020 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 darwin && !ios +// +build darwin,!ios + +#import + +#include "_cgo_export.h" + +@interface OtoNotificationObserver : NSObject { +} + +@end + +@implementation OtoNotificationObserver { +} + +- (void)receiveSleepNote:(NSNotification *)note { + oto_setGlobalPause(); +} + +- (void)receiveWakeNote:(NSNotification *)note { + oto_setGlobalResume(); +} + +@end + +// oto_setNotificationHandler sets a handler for sleep/wake +// notifications. +void oto_setNotificationHandler() { + OtoNotificationObserver *observer = [[OtoNotificationObserver alloc] init]; + + [[[NSWorkspace sharedWorkspace] notificationCenter] + addObserver:observer + selector:@selector(receiveSleepNote:) + name:NSWorkspaceWillSleepNotification + object:NULL]; + [[[NSWorkspace sharedWorkspace] notificationCenter] + addObserver:observer + selector:@selector(receiveWakeNote:) + name:NSWorkspaceDidWakeNotification + object:NULL]; +} diff --git a/go.mod b/go.mod index b72023d..e48a23c 100644 --- a/go.mod +++ b/go.mod @@ -2,9 +2,6 @@ module github.com/hajimehoshi/oto/v2 go 1.16 -require ( - github.com/ebitengine/purego v0.0.0-20220729024107-78cdc2949de6 - golang.org/x/sys v0.0.0-20220712014510-0a85c31ab51e -) +require golang.org/x/sys v0.0.0-20220712014510-0a85c31ab51e retract v2.3.0-alpha.4 // Wrongly tagged diff --git a/go.sum b/go.sum index 98579df..975cf95 100644 --- a/go.sum +++ b/go.sum @@ -1,4 +1,2 @@ -github.com/ebitengine/purego v0.0.0-20220729024107-78cdc2949de6 h1:kc3Im5Yj31aW0r3VdXHk+urwClkQilDBAYYp0TXeqE8= -github.com/ebitengine/purego v0.0.0-20220729024107-78cdc2949de6/go.mod h1:Eh8I3yvknDYZeCuXH9kRNaPuHEwvXDCk378o9xszmHg= golang.org/x/sys v0.0.0-20220712014510-0a85c31ab51e h1:NHvCuwuS43lGnYhten69ZWqi2QOj/CiDNcKbVqwVoew= golang.org/x/sys v0.0.0-20220712014510-0a85c31ab51e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=