mirror of
https://github.com/kyleneideck/BackgroundMusic
synced 2024-11-10 06:34:22 +00:00
Fix a deadlock when changing output device while IO is running.
BGM_Device::StartIO blocks on BGMAudioDeviceManager::waitForOutputDeviceToStart, which could be blocked by HAL requests that the HAL wouldn't return until BGM_Device::StartIO returned. Also: - Replace BGMPlayThrough's move constructor with a SetDevices function for simplicity. - Pause/abort debug builds if an error is logged.
This commit is contained in:
parent
467b072a9d
commit
a91615fc5e
10 changed files with 347 additions and 194 deletions
|
@ -922,6 +922,7 @@
|
|||
"CoreAudio_ThreadStampMessages=1",
|
||||
"BGM_StopDebuggerOnLoggedExceptions=$(BGM_STOP_DEBUGGER_ON_LOGGED_EXCEPTIONS)",
|
||||
"BGM_StopDebuggerOnLoggedUnexpectedExceptions=$(BGM_STOP_DEBUGGER_ON_LOGGED_UNEXPECTED_EXCEPTIONS)",
|
||||
"CoreAudio_StopOnThrow=0",
|
||||
);
|
||||
GCC_TREAT_IMPLICIT_FUNCTION_DECLARATIONS_AS_ERRORS = YES;
|
||||
GCC_TREAT_WARNINGS_AS_ERRORS = YES;
|
||||
|
@ -1033,6 +1034,7 @@
|
|||
"CoreAudio_ThreadStampMessages=1",
|
||||
"BGM_StopDebuggerOnLoggedExceptions=$(BGM_STOP_DEBUGGER_ON_LOGGED_EXCEPTIONS)",
|
||||
"BGM_StopDebuggerOnLoggedUnexpectedExceptions=$(BGM_STOP_DEBUGGER_ON_LOGGED_UNEXPECTED_EXCEPTIONS)",
|
||||
"CoreAudio_StopOnThrow=0",
|
||||
);
|
||||
GCC_TREAT_IMPLICIT_FUNCTION_DECLARATIONS_AS_ERRORS = YES;
|
||||
GCC_TREAT_WARNINGS_AS_ERRORS = YES;
|
||||
|
@ -1105,6 +1107,7 @@
|
|||
"CoreAudio_StopOnAssert=0",
|
||||
"BGM_StopDebuggerOnLoggedExceptions=$(BGM_STOP_DEBUGGER_ON_LOGGED_EXCEPTIONS)",
|
||||
"BGM_StopDebuggerOnLoggedUnexpectedExceptions=$(BGM_STOP_DEBUGGER_ON_LOGGED_UNEXPECTED_EXCEPTIONS)",
|
||||
"CoreAudio_StopOnThrow=0",
|
||||
);
|
||||
GCC_TREAT_IMPLICIT_FUNCTION_DECLARATIONS_AS_ERRORS = YES;
|
||||
GCC_TREAT_WARNINGS_AS_ERRORS = YES;
|
||||
|
|
|
@ -17,7 +17,7 @@
|
|||
// BGMAudioDeviceManager.mm
|
||||
// BGMApp
|
||||
//
|
||||
// Copyright © 2016 Kyle Neideck
|
||||
// Copyright © 2016, 2017 Kyle Neideck
|
||||
//
|
||||
|
||||
// Self Include
|
||||
|
@ -47,33 +47,41 @@ public:
|
|||
@implementation BGMAudioDeviceManager {
|
||||
BGMAudioDevice bgmDevice;
|
||||
BGMAudioDevice outputDevice;
|
||||
|
||||
BGMDeviceControlSync deviceControlSync;
|
||||
BGMPlayThrough playThrough;
|
||||
|
||||
NSRecursiveLock* stateLock;
|
||||
}
|
||||
|
||||
#pragma mark Construction/Destruction
|
||||
|
||||
- (id) initWithError:(NSError**)error {
|
||||
if ((self = [super init])) {
|
||||
stateLock = [NSRecursiveLock new];
|
||||
|
||||
bgmDevice = BGMAudioDevice(CFSTR(kBGMDeviceUID));
|
||||
|
||||
if (bgmDevice.GetObjectID() == kAudioObjectUnknown) {
|
||||
LogError("BGMAudioDeviceManager::initWithError: BGMDevice not found");
|
||||
|
||||
if (error) {
|
||||
*error = [NSError errorWithDomain:@kBGMAppBundleID code:kBGMErrorCode_BGMDeviceNotFound userInfo:nil];
|
||||
}
|
||||
|
||||
self = nil;
|
||||
return self;
|
||||
}
|
||||
|
||||
[self initOutputDevice];
|
||||
|
||||
|
||||
if (outputDevice.GetObjectID() == kAudioDeviceUnknown) {
|
||||
LogError("BGMAudioDeviceManager::initWithError: output device not found");
|
||||
|
||||
if (error) {
|
||||
*error = [NSError errorWithDomain:@kBGMAppBundleID code:kBGMErrorCode_OutputDeviceNotFound userInfo:nil];
|
||||
}
|
||||
|
||||
self = nil;
|
||||
return self;
|
||||
}
|
||||
|
@ -86,6 +94,7 @@ public:
|
|||
CAHALAudioSystemObject audioSystem;
|
||||
// outputDevice = BGMAudioDevice(CFSTR("AppleHDAEngineOutput:1B,0,1,1:0"));
|
||||
AudioObjectID defaultDeviceID = audioSystem.GetDefaultAudioDevice(false, false);
|
||||
|
||||
if (defaultDeviceID == bgmDevice.GetObjectID()) {
|
||||
// TODO: If BGMDevice is already the default (because BGMApp didn't shutdown properly or it was set manually)
|
||||
// we should temporarily disable BGMDevice so we can find out what the previous default was.
|
||||
|
@ -95,6 +104,7 @@ public:
|
|||
if (numDevices > 0) {
|
||||
SInt32 minLatencyDeviceIdx = -1;
|
||||
UInt32 minLatency = UINT32_MAX;
|
||||
|
||||
CAAutoArrayDelete<AudioObjectID> devices(numDevices);
|
||||
audioSystem.GetAudioDevices(numDevices, devices);
|
||||
|
||||
|
@ -155,13 +165,16 @@ public:
|
|||
AudioDeviceID bgmDeviceID = kAudioDeviceUnknown;
|
||||
AudioDeviceID outputDeviceID = kAudioDeviceUnknown;
|
||||
|
||||
@synchronized (self) {
|
||||
@try {
|
||||
[stateLock lock];
|
||||
BGMLogAndSwallowExceptions("setBGMDeviceAsOSDefault", [&]() {
|
||||
bgmDeviceID = bgmDevice.GetObjectID();
|
||||
outputDeviceID = outputDevice.GetObjectID();
|
||||
});
|
||||
} @finally {
|
||||
[stateLock unlock];
|
||||
}
|
||||
|
||||
|
||||
if (outputDeviceID == kAudioDeviceUnknown) {
|
||||
return [NSError errorWithDomain:@kBGMAppBundleID code:kBGMErrorCode_OutputDeviceNotFound userInfo:nil];
|
||||
}
|
||||
|
@ -200,7 +213,8 @@ public:
|
|||
AudioDeviceID bgmDeviceID = kAudioDeviceUnknown;
|
||||
AudioDeviceID outputDeviceID = kAudioDeviceUnknown;
|
||||
|
||||
@synchronized (self) {
|
||||
@try {
|
||||
[stateLock lock];
|
||||
BGMLogAndSwallowExceptions("unsetBGMDeviceAsOSDefault", [&]() {
|
||||
bgmDeviceID = bgmDevice.GetObjectID();
|
||||
outputDeviceID = outputDevice.GetObjectID();
|
||||
|
@ -211,8 +225,10 @@ public:
|
|||
bgmDeviceIsSystemDefault =
|
||||
(audioSystem.GetDefaultAudioDevice(false, true) == bgmDeviceID);
|
||||
});
|
||||
} @finally {
|
||||
[stateLock unlock];
|
||||
}
|
||||
|
||||
|
||||
if (outputDeviceID == kAudioDeviceUnknown) {
|
||||
return [NSError errorWithDomain:@kBGMAppBundleID code:kBGMErrorCode_OutputDeviceNotFound userInfo:nil];
|
||||
}
|
||||
|
@ -260,13 +276,18 @@ public:
|
|||
}
|
||||
|
||||
- (BOOL) isOutputDevice:(AudioObjectID)deviceID {
|
||||
@synchronized (self) {
|
||||
@try {
|
||||
[stateLock lock];
|
||||
return deviceID == outputDevice.GetObjectID();
|
||||
} @finally {
|
||||
[stateLock unlock];
|
||||
}
|
||||
}
|
||||
|
||||
- (BOOL) isOutputDataSource:(UInt32)dataSourceID {
|
||||
@synchronized (self) {
|
||||
@try {
|
||||
[stateLock lock];
|
||||
|
||||
try {
|
||||
AudioObjectPropertyScope scope = kAudioDevicePropertyScopeOutput;
|
||||
UInt32 channel = 0;
|
||||
|
@ -277,6 +298,8 @@ public:
|
|||
BGMLogException(e);
|
||||
return false;
|
||||
}
|
||||
} @finally {
|
||||
[stateLock unlock];
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -307,10 +330,12 @@ public:
|
|||
// Set up playthrough and control sync
|
||||
BGMAudioDevice newOutputDevice(newDeviceID);
|
||||
|
||||
try {
|
||||
@synchronized (self) {
|
||||
@try {
|
||||
[stateLock lock];
|
||||
|
||||
try {
|
||||
// Re-read the device ID after entering the monitor. (The initial read is because
|
||||
// currentDeviceID's used in the catch blocks.)
|
||||
// currentDeviceID is used in the catch blocks.)
|
||||
currentDeviceID = outputDevice.GetObjectID();
|
||||
|
||||
if (newDeviceID != currentDeviceID) {
|
||||
|
@ -319,7 +344,7 @@ public:
|
|||
|
||||
// Stream audio from BGMDevice to the new output device. This blocks while the old device
|
||||
// stops IO.
|
||||
playThrough = BGMPlayThrough(bgmDevice, newOutputDevice);
|
||||
playThrough.SetDevices(&bgmDevice, &newOutputDevice);
|
||||
|
||||
outputDevice = newOutputDevice;
|
||||
}
|
||||
|
@ -339,20 +364,22 @@ public:
|
|||
// But stop playthrough if audio isn't playing, since it uses CPU.
|
||||
playThrough.StopIfIdle();
|
||||
}
|
||||
} catch (CAException e) {
|
||||
BGMAssert(e.GetError() != kAudioHardwareNoError,
|
||||
"CAException with kAudioHardwareNoError");
|
||||
|
||||
return [self failedToSetOutputDevice:newDeviceID
|
||||
errorCode:e.GetError()
|
||||
revertTo:(revertOnFailure ? ¤tDeviceID : nullptr)];
|
||||
} catch (...) {
|
||||
return [self failedToSetOutputDevice:newDeviceID
|
||||
errorCode:kAudioHardwareUnspecifiedError
|
||||
revertTo:(revertOnFailure ? ¤tDeviceID : nullptr)];
|
||||
}
|
||||
} catch (CAException e) {
|
||||
BGMAssert(e.GetError() != kAudioHardwareNoError,
|
||||
"CAException with kAudioHardwareNoError");
|
||||
|
||||
return [self failedToSetOutputDevice:newDeviceID
|
||||
errorCode:e.GetError()
|
||||
revertTo:(revertOnFailure ? ¤tDeviceID : nullptr)];
|
||||
} catch (...) {
|
||||
return [self failedToSetOutputDevice:newDeviceID
|
||||
errorCode:kAudioHardwareUnspecifiedError
|
||||
revertTo:(revertOnFailure ? ¤tDeviceID : nullptr)];
|
||||
} @finally {
|
||||
[stateLock unlock];
|
||||
}
|
||||
|
||||
|
||||
return nil;
|
||||
}
|
||||
|
||||
|
@ -400,9 +427,55 @@ public:
|
|||
}
|
||||
|
||||
- (OSStatus) waitForOutputDeviceToStart {
|
||||
// Intentionally not synchronized to avoid blocking the UI thread. BGMPlayThrough::WaitForOutputDeviceToStart
|
||||
// will be interrupted if the output device is changed.
|
||||
return playThrough.WaitForOutputDeviceToStart();
|
||||
// We can only try for stateLock because setOutputDeviceWithID might have already taken it, then made a
|
||||
// HAL request to BGMDevice and is now waiting for the response. Some of the requests setOutputDeviceWithID
|
||||
// makes to BGMDevice block in the HAL if another thread is in BGM_Device::StartIO.
|
||||
//
|
||||
// Since BGM_Device::StartIO calls this method (via XPC), waiting for setOutputDeviceWithID to release
|
||||
// stateLock could cause deadlocks. Instead we return early with an error code that BGMDriver knows to
|
||||
// ignore, since the output device is (almost certainly) being changed and we can't avoid dropping frames
|
||||
// while the output device starts up.
|
||||
OSStatus err;
|
||||
BOOL gotLock;
|
||||
|
||||
@try {
|
||||
gotLock = [stateLock tryLock];
|
||||
|
||||
if (gotLock) {
|
||||
err = playThrough.WaitForOutputDeviceToStart();
|
||||
} else {
|
||||
LogWarning("BGMAudioDeviceManager::waitForOutputDeviceToStart: Didn't get state lock. Returning "
|
||||
"early with kDeviceNotStarting.");
|
||||
err = BGMPlayThrough::kDeviceNotStarting;
|
||||
}
|
||||
|
||||
if (err == BGMPlayThrough::kDeviceNotStarting) {
|
||||
// I'm not sure if this block is currently reachable, but BGMDriver only starts waiting on the
|
||||
// output device when IO is starting, so we should start playthrough even if BGMApp hasn't been
|
||||
// notified by the HAL yet.
|
||||
LogWarning("BGMAudioDeviceManager::waitForOutputDeviceToStart: Playthrough wasn't starting the "
|
||||
"output device. Will tell it to and then return early with kDeviceNotStarting.");
|
||||
|
||||
dispatch_async(dispatch_get_global_queue(QOS_CLASS_USER_INTERACTIVE, 0), ^{
|
||||
@try {
|
||||
[stateLock lock];
|
||||
|
||||
BGMLogAndSwallowExceptions("BGMAudioDeviceManager::waitForOutputDeviceToStart", [&]() {
|
||||
playThrough.Start();
|
||||
playThrough.StopIfIdle();
|
||||
});
|
||||
} @finally {
|
||||
[stateLock unlock];
|
||||
}
|
||||
});
|
||||
}
|
||||
} @finally {
|
||||
if (gotLock) {
|
||||
[stateLock unlock];
|
||||
}
|
||||
}
|
||||
|
||||
return err;
|
||||
}
|
||||
|
||||
@end
|
||||
|
|
|
@ -17,7 +17,7 @@
|
|||
// BGMPlayThrough.cpp
|
||||
// BGMApp
|
||||
//
|
||||
// Copyright © 2016 Kyle Neideck
|
||||
// Copyright © 2016, 2017 Kyle Neideck
|
||||
//
|
||||
|
||||
// Self Include
|
||||
|
@ -59,73 +59,53 @@ BGMPlayThrough::BGMPlayThrough(CAHALAudioDevice inInputDevice, CAHALAudioDevice
|
|||
mInputDevice(inInputDevice),
|
||||
mOutputDevice(inOutputDevice)
|
||||
{
|
||||
BGMAssert(mInputDeviceIOProcState.is_lock_free(),
|
||||
"BGMPlayThrough::BGMPlayThrough: !mInputDeviceIOProcState.is_lock_free()");
|
||||
BGMAssert(mOutputDeviceIOProcState.is_lock_free(),
|
||||
"BGMPlayThrough::BGMPlayThrough: !mOutputDeviceIOProcState.is_lock_free()");
|
||||
|
||||
AllocateBuffer();
|
||||
|
||||
// Init the semaphore for the output IOProc.
|
||||
kern_return_t theError = semaphore_create(mach_task_self(), &mOutputDeviceIOProcSemaphore, SYNC_POLICY_FIFO, 0);
|
||||
BGM_Utils::ThrowIfMachError("BGMPlayThrough::BGMPlayThrough", "semaphore_create", theError);
|
||||
|
||||
ThrowIf(mOutputDeviceIOProcSemaphore == SEMAPHORE_NULL,
|
||||
CAException(kAudioHardwareUnspecifiedError),
|
||||
"BGMPlayThrough::BGMPlayThrough: Could not create semaphore");
|
||||
Init(inInputDevice, inOutputDevice);
|
||||
}
|
||||
|
||||
BGMPlayThrough::~BGMPlayThrough()
|
||||
{
|
||||
BGMLogAndSwallowExceptions("~BGMPlayThrough", [&]() {
|
||||
BGMLogAndSwallowExceptionsMsg("BGMPlayThrough::~BGMPlayThrough", "Deactivate", [&]() {
|
||||
Deactivate();
|
||||
|
||||
if(mOutputDeviceIOProcSemaphore != SEMAPHORE_NULL)
|
||||
{
|
||||
kern_return_t theError = semaphore_destroy(mach_task_self(), mOutputDeviceIOProcSemaphore);
|
||||
BGM_Utils::ThrowIfMachError("BGMPlayThrough::~BGMPlayThrough", "semaphore_destroy", theError);
|
||||
}
|
||||
});
|
||||
|
||||
if(mOutputDeviceIOProcSemaphore != SEMAPHORE_NULL)
|
||||
{
|
||||
kern_return_t theError = semaphore_destroy(mach_task_self(), mOutputDeviceIOProcSemaphore);
|
||||
BGM_Utils::LogIfMachError("BGMPlayThrough::~BGMPlayThrough", "semaphore_destroy", theError);
|
||||
}
|
||||
}
|
||||
|
||||
void BGMPlayThrough::Swap(BGMPlayThrough& inPlayThrough)
|
||||
void BGMPlayThrough::Init(CAHALAudioDevice inInputDevice, CAHALAudioDevice inOutputDevice)
|
||||
{
|
||||
if(this == &inPlayThrough)
|
||||
BGMAssert(mInputDeviceIOProcState.is_lock_free(),
|
||||
"BGMPlayThrough::BGMPlayThrough: !mInputDeviceIOProcState.is_lock_free()");
|
||||
BGMAssert(mOutputDeviceIOProcState.is_lock_free(),
|
||||
"BGMPlayThrough::BGMPlayThrough: !mOutputDeviceIOProcState.is_lock_free()");
|
||||
BGMAssert(!mActive, "BGMPlayThrough::BGMPlayThrough: Can't init while active.");
|
||||
|
||||
mInputDevice = inInputDevice;
|
||||
mOutputDevice = inOutputDevice;
|
||||
|
||||
AllocateBuffer();
|
||||
|
||||
try
|
||||
{
|
||||
return;
|
||||
// Init the semaphore for the output IOProc.
|
||||
if(mOutputDeviceIOProcSemaphore == SEMAPHORE_NULL)
|
||||
{
|
||||
kern_return_t theError = semaphore_create(mach_task_self(), &mOutputDeviceIOProcSemaphore, SYNC_POLICY_FIFO, 0);
|
||||
BGM_Utils::ThrowIfMachError("BGMPlayThrough::BGMPlayThrough", "semaphore_create", theError);
|
||||
|
||||
ThrowIf(mOutputDeviceIOProcSemaphore == SEMAPHORE_NULL,
|
||||
CAException(kAudioHardwareUnspecifiedError),
|
||||
"BGMPlayThrough::BGMPlayThrough: Could not create semaphore");
|
||||
}
|
||||
}
|
||||
|
||||
CAMutex::Locker stateLocker(mStateMutex);
|
||||
|
||||
bool wasPlayingThrough = inPlayThrough.mPlayingThrough;
|
||||
|
||||
BGMLogAndSwallowExceptions("BGMPlayThrough::Swap", [&]() {
|
||||
Deactivate();
|
||||
});
|
||||
|
||||
mInputDevice = inPlayThrough.mInputDevice;
|
||||
mOutputDevice = inPlayThrough.mOutputDevice;
|
||||
|
||||
// Steal inPlayThrough's semaphore if this object needs one.
|
||||
if(mOutputDeviceIOProcSemaphore == SEMAPHORE_NULL)
|
||||
catch (...)
|
||||
{
|
||||
mOutputDeviceIOProcSemaphore = inPlayThrough.mOutputDeviceIOProcSemaphore;
|
||||
inPlayThrough.mOutputDeviceIOProcSemaphore = SEMAPHORE_NULL;
|
||||
}
|
||||
|
||||
BGMLogAndSwallowExceptions("BGMPlayThrough::Swap", [&]() {
|
||||
AllocateBuffer();
|
||||
});
|
||||
|
||||
BGMLogAndSwallowExceptions("BGMPlayThrough::Swap", [&]() {
|
||||
inPlayThrough.Deactivate();
|
||||
});
|
||||
|
||||
if(wasPlayingThrough)
|
||||
{
|
||||
BGMLogAndSwallowExceptions("BGMPlayThrough::Swap", [&]() {
|
||||
Start();
|
||||
});
|
||||
// Clean up.
|
||||
mBuffer.Deallocate();
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -352,6 +332,9 @@ void BGMPlayThrough::DestroyIOProcIDs()
|
|||
DebugMsg("BGMPlayThrough::DestroyIOProcIDs: Destroying IOProcs");
|
||||
|
||||
auto destroy = [](CAHALAudioDevice& device, const char* deviceName, AudioDeviceIOProcID& ioProcID) {
|
||||
#if !DEBUG
|
||||
#pragma unused (deviceName)
|
||||
#endif
|
||||
if(ioProcID != nullptr)
|
||||
{
|
||||
try
|
||||
|
@ -404,6 +387,38 @@ bool BGMPlayThrough::CheckIOProcsAreStopped() const noexcept
|
|||
return statesOK;
|
||||
}
|
||||
|
||||
void BGMPlayThrough::SetDevices(CAHALAudioDevice* __nullable inInputDevice,
|
||||
CAHALAudioDevice* __nullable inOutputDevice)
|
||||
{
|
||||
CAMutex::Locker stateLocker(mStateMutex);
|
||||
|
||||
bool wasActive = mActive;
|
||||
bool wasPlayingThrough = mPlayingThrough;
|
||||
|
||||
if(wasPlayingThrough)
|
||||
{
|
||||
BGMAssert(wasActive, "BGMPlayThrough::SetOutputDevice: wasPlayingThrough && !wasActive"); // Sanity check.
|
||||
}
|
||||
|
||||
Deactivate();
|
||||
|
||||
mInputDevice = inInputDevice ? *inInputDevice : mInputDevice;
|
||||
mOutputDevice = inOutputDevice ? *inOutputDevice : mOutputDevice;
|
||||
|
||||
// Resize and reallocate the buffer if necessary.
|
||||
Init(mInputDevice, mOutputDevice);
|
||||
|
||||
if(wasActive)
|
||||
{
|
||||
Activate();
|
||||
}
|
||||
|
||||
if(wasPlayingThrough)
|
||||
{
|
||||
Start();
|
||||
}
|
||||
}
|
||||
|
||||
#pragma mark Control Playthrough
|
||||
|
||||
void BGMPlayThrough::Start()
|
||||
|
@ -443,42 +458,34 @@ void BGMPlayThrough::Start()
|
|||
|
||||
DebugMsg("BGMPlayThrough::Start: Starting playthrough");
|
||||
|
||||
mInputDeviceIOProcState = IOState::Starting;
|
||||
mOutputDeviceIOProcState = IOState::Starting;
|
||||
|
||||
// Start our IOProcs
|
||||
// Start our IOProcs.
|
||||
try
|
||||
{
|
||||
mInputDeviceIOProcState = IOState::Starting;
|
||||
mInputDevice.StartIOProc(mInputDeviceIOProcID);
|
||||
|
||||
mOutputDeviceIOProcState = IOState::Starting;
|
||||
mOutputDevice.StartIOProc(mOutputDeviceIOProcID);
|
||||
}
|
||||
catch(...)
|
||||
catch(CAException e)
|
||||
{
|
||||
ReleaseThreadsWaitingForOutputToStart();
|
||||
|
||||
LogError("BGMPlayThrough::Start: Failed to start input device");
|
||||
// Log an error message.
|
||||
OSStatus err = e.GetError();
|
||||
char err4CC[5] = CA4CCToCString(err);
|
||||
LogError("BGMPlayThrough::Start: Failed to start %s device. Error: %d (%s)",
|
||||
(mOutputDeviceIOProcState == IOState::Starting ? "output" : "input"),
|
||||
err,
|
||||
err4CC);
|
||||
|
||||
// Try to stop the IOProcs in case StartIOProc failed because one of our IOProc was already
|
||||
// running. I don't know if it actually does fail in that case, but the documentation
|
||||
// doesn't say so it's safer to assume it could.
|
||||
CATry
|
||||
mInputDevice.StopIOProc(mInputDeviceIOProcID);
|
||||
CACatch
|
||||
|
||||
mInputDeviceIOProcState = IOState::Stopped;
|
||||
mOutputDeviceIOProcState = IOState::Stopped;
|
||||
|
||||
throw;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
mOutputDevice.StartIOProc(mOutputDeviceIOProcID);
|
||||
}
|
||||
catch(...)
|
||||
{
|
||||
ReleaseThreadsWaitingForOutputToStart();
|
||||
|
||||
LogError("BGMPlayThrough::Start: Failed to start output device");
|
||||
|
||||
CATry
|
||||
mInputDevice.StopIOProc(mInputDeviceIOProcID);
|
||||
mOutputDevice.StopIOProc(mOutputDeviceIOProcID);
|
||||
CACatch
|
||||
|
||||
|
@ -493,16 +500,14 @@ void BGMPlayThrough::Start()
|
|||
|
||||
OSStatus BGMPlayThrough::WaitForOutputDeviceToStart() noexcept
|
||||
{
|
||||
semaphore_t semaphore;
|
||||
IOState state;
|
||||
UInt64 startedAt = mach_absolute_time();
|
||||
|
||||
// Check for errors.
|
||||
//
|
||||
// Technically we should take the state mutex here, but that could cause deadlocks because
|
||||
// BGM_Device::StartIO (in BGMDriver) blocks on this function (via XPC). Other BGMPlayThrough
|
||||
// functions make requests to BGMDriver while holding the state mutex, usually to get/set
|
||||
// properties, but the HAL will block those requests until BGM_Device::StartIO returns.
|
||||
try
|
||||
{
|
||||
// Note that we only hold the state mutex while setting up to wait.
|
||||
CAMutex::Locker stateLocker(mStateMutex);
|
||||
|
||||
// Check for errors.
|
||||
if(!mActive)
|
||||
{
|
||||
LogError("BGMPlayThrough::WaitForOutputDeviceToStart: !mActive");
|
||||
|
@ -514,30 +519,31 @@ OSStatus BGMPlayThrough::WaitForOutputDeviceToStart() noexcept
|
|||
LogError("BGMPlayThrough::WaitForOutputDeviceToStart: Device not alive");
|
||||
return kAudioHardwareBadDeviceError;
|
||||
}
|
||||
|
||||
// Return early if the output device is already running.
|
||||
state = mOutputDeviceIOProcState;
|
||||
if(state == IOState::Running)
|
||||
{
|
||||
return kAudioHardwareNoError;
|
||||
}
|
||||
|
||||
// Return an error if we haven't been told to start the output device yet. (I.e. we haven't
|
||||
// received a kAudioDevicePropertyDeviceIsRunning notification.)
|
||||
if(state != IOState::Starting)
|
||||
{
|
||||
LogError("BGMPlayThrough::WaitForOutputDeviceToStart: Device not starting");
|
||||
return kAudioHardwareIllegalOperationError;
|
||||
}
|
||||
|
||||
// Copy the semaphore into a local so we don't have to hold the mutex while waiting.
|
||||
semaphore = mOutputDeviceIOProcSemaphore;
|
||||
}
|
||||
catch(CAException e)
|
||||
{
|
||||
BGMLogException(e);
|
||||
return e.GetError();
|
||||
}
|
||||
|
||||
const IOState initialState = mOutputDeviceIOProcState;
|
||||
const UInt64 startedAt = mach_absolute_time();
|
||||
|
||||
if(initialState == IOState::Running)
|
||||
{
|
||||
// Return early because the output device is already running.
|
||||
return kAudioHardwareNoError;
|
||||
}
|
||||
else if(initialState != IOState::Starting)
|
||||
{
|
||||
// Warn if we haven't been told to start the output device yet. Usually means we
|
||||
// haven't received a kAudioDevicePropertyDeviceIsRunning notification yet, which can
|
||||
// happen. It's most common when the user changes the output device while IO is
|
||||
// running.
|
||||
LogWarning("BGMPlayThrough::WaitForOutputDeviceToStart: Device not starting");
|
||||
|
||||
return kDeviceNotStarting;
|
||||
}
|
||||
|
||||
// Wait for our output IOProc to start. mOutputDeviceIOProcSemaphore is reset to 0
|
||||
// (semaphore_signal_all) when our IOProc is running on the output device.
|
||||
|
@ -547,18 +553,22 @@ OSStatus BGMPlayThrough::WaitForOutputDeviceToStart() noexcept
|
|||
// changes immediately after we call StartIOProc.)
|
||||
//
|
||||
// We check mOutputDeviceIOProcState every 200ms as a fault tolerance mechanism. (Though,
|
||||
// I'm not completely sure it's impossible to be woken spuriously and miss the signal from
|
||||
// the IOProc, so it might actually be necessary.)
|
||||
// I'm not completely sure it's impossible to miss the signal from the IOProc because of a
|
||||
// spurious wake up, so it might actually be necessary.)
|
||||
DebugMsg("BGMPlayThrough::WaitForOutputDeviceToStart: Waiting.");
|
||||
|
||||
kern_return_t theError;
|
||||
IOState state;
|
||||
UInt64 waitedNsec = 0;
|
||||
mach_timebase_info_data_t info;
|
||||
mach_timebase_info(&info);
|
||||
|
||||
do
|
||||
{
|
||||
theError = semaphore_timedwait(semaphore,
|
||||
BGMAssert(mOutputDeviceIOProcSemaphore != SEMAPHORE_NULL,
|
||||
"BGMPlayThrough::WaitForOutputDeviceToStart: !mOutputDeviceIOProcSemaphore");
|
||||
|
||||
theError = semaphore_timedwait(mOutputDeviceIOProcSemaphore,
|
||||
(mach_timespec_t){ 0, 200 * NSEC_PER_MSEC });
|
||||
|
||||
// Update the total time we've been waiting and the output device's state.
|
||||
|
@ -661,25 +671,25 @@ OSStatus BGMPlayThrough::Stop()
|
|||
// when you make the call from outside of your IOProc. However, if you call AudioDeviceStop() from inside your IOProc,
|
||||
// you do get the guarantee that your IOProc will not get called again after the IOProc has returned.
|
||||
UInt64 totalWaitNs = 0;
|
||||
CATry
|
||||
Float64 expectedInputCycleNs =
|
||||
mInputDevice.GetIOBufferSize() * (1 / mInputDevice.GetNominalSampleRate()) * NSEC_PER_SEC;
|
||||
Float64 expectedOutputCycleNs =
|
||||
mOutputDevice.GetIOBufferSize() * (1 / mOutputDevice.GetNominalSampleRate()) * NSEC_PER_SEC;
|
||||
UInt64 expectedMaxCycleNs =
|
||||
static_cast<UInt64>(std::max(expectedInputCycleNs, expectedOutputCycleNs));
|
||||
|
||||
while((mInputDeviceIOProcState == IOState::Stopping || mOutputDeviceIOProcState == IOState::Stopping)
|
||||
&& (totalWaitNs < 4 * expectedMaxCycleNs))
|
||||
{
|
||||
// TODO: If playthrough is started again while we're waiting in this loop we could drop frames. Wait on a semaphore
|
||||
// instead of sleeping? That way Start() could also signal it, before waiting on the state mutex, as a way of
|
||||
// cancelling the stop operation.
|
||||
struct timespec rmtp;
|
||||
int err = nanosleep((const struct timespec[]){{0, NSEC_PER_MSEC}}, &rmtp);
|
||||
totalWaitNs += NSEC_PER_MSEC - (err == -1 ? rmtp.tv_nsec : 0);
|
||||
}
|
||||
CACatch
|
||||
BGMLogAndSwallowExceptions("BGMPlayThrough::Stop", [&]() {
|
||||
Float64 expectedInputCycleNs =
|
||||
mInputDevice.GetIOBufferSize() * (1 / mInputDevice.GetNominalSampleRate()) * NSEC_PER_SEC;
|
||||
Float64 expectedOutputCycleNs =
|
||||
mOutputDevice.GetIOBufferSize() * (1 / mOutputDevice.GetNominalSampleRate()) * NSEC_PER_SEC;
|
||||
UInt64 expectedMaxCycleNs =
|
||||
static_cast<UInt64>(std::max(expectedInputCycleNs, expectedOutputCycleNs));
|
||||
|
||||
while((mInputDeviceIOProcState == IOState::Stopping || mOutputDeviceIOProcState == IOState::Stopping)
|
||||
&& (totalWaitNs < 4 * expectedMaxCycleNs))
|
||||
{
|
||||
// TODO: If playthrough is started again while we're waiting in this loop we could drop frames. Wait on a
|
||||
// semaphore instead of sleeping? That way Start() could also signal it, before waiting on the state mutex,
|
||||
// as a way of cancelling the stop operation.
|
||||
struct timespec rmtp;
|
||||
int err = nanosleep((const struct timespec[]){{0, NSEC_PER_MSEC}}, &rmtp);
|
||||
totalWaitNs += NSEC_PER_MSEC - (err == -1 ? rmtp.tv_nsec : 0);
|
||||
}
|
||||
});
|
||||
|
||||
// Clean up if the IOProcs didn't stop themselves
|
||||
if(mInputDeviceIOProcState == IOState::Stopping && mInputDeviceIOProcID != nullptr)
|
||||
|
@ -713,7 +723,7 @@ OSStatus BGMPlayThrough::Stop()
|
|||
mLastInputSampleTime = -1;
|
||||
mLastOutputSampleTime = -1;
|
||||
|
||||
return noErr;
|
||||
return noErr; // TODO: Why does this return anything and why always noErr?
|
||||
}
|
||||
|
||||
void BGMPlayThrough::StopIfIdle()
|
||||
|
|
|
@ -17,7 +17,7 @@
|
|||
// BGMPlayThrough.h
|
||||
// BGMApp
|
||||
//
|
||||
// Copyright © 2016 Kyle Neideck
|
||||
// Copyright © 2016, 2017 Kyle Neideck
|
||||
//
|
||||
// Reads audio from an input device and immediately writes it to an output device. We currently use this class with the input
|
||||
// device always set to BGMDevice and the output device set to the one selected in the preferences menu.
|
||||
|
@ -46,6 +46,7 @@
|
|||
|
||||
// STL Includes
|
||||
#include <atomic>
|
||||
#include <algorithm>
|
||||
|
||||
// System Includes
|
||||
#include <mach/semaphore.h>
|
||||
|
@ -55,6 +56,10 @@
|
|||
|
||||
class BGMPlayThrough
|
||||
{
|
||||
|
||||
public:
|
||||
// Error codes
|
||||
static const OSStatus kDeviceNotStarting = 100;
|
||||
|
||||
public:
|
||||
BGMPlayThrough(CAHALAudioDevice inInputDevice, CAHALAudioDevice inOutputDevice);
|
||||
|
@ -62,17 +67,16 @@ public:
|
|||
// Disallow copying
|
||||
BGMPlayThrough(const BGMPlayThrough&) = delete;
|
||||
BGMPlayThrough& operator=(const BGMPlayThrough&) = delete;
|
||||
// Move constructor/assignment
|
||||
BGMPlayThrough(BGMPlayThrough&& inPlayThrough) { Swap(inPlayThrough); }
|
||||
BGMPlayThrough& operator=(BGMPlayThrough&& inPlayThrough) { Swap(inPlayThrough); return *this; }
|
||||
|
||||
#ifdef __OBJC__
|
||||
// Only intended as a convenience for Objective-C instance vars
|
||||
// Only intended as a convenience (hack) for Objective-C instance vars. Call
|
||||
// SetDevices to initialise the instance before using it.
|
||||
BGMPlayThrough() { }
|
||||
#endif
|
||||
|
||||
private:
|
||||
void Swap(BGMPlayThrough& inPlayThrough);
|
||||
/*! @throws CAException */
|
||||
void Init(CAHALAudioDevice inInputDevice, CAHALAudioDevice inOutputDevice);
|
||||
|
||||
/*! @throws CAException */
|
||||
void Activate();
|
||||
|
@ -94,6 +98,13 @@ private:
|
|||
bool CheckIOProcsAreStopped() const noexcept; // TODO: REQUIRES(mStateMutex);
|
||||
|
||||
public:
|
||||
/*!
|
||||
Pass null for either param to only change one of the devices.
|
||||
@throws CAException
|
||||
*/
|
||||
void SetDevices(CAHALAudioDevice* __nullable inInputDevice,
|
||||
CAHALAudioDevice* __nullable inOutputDevice);
|
||||
|
||||
/*! @throws CAException */
|
||||
void Start();
|
||||
|
||||
|
@ -140,6 +151,7 @@ private:
|
|||
{
|
||||
Stopped, Starting, Running, Stopping
|
||||
};
|
||||
|
||||
// The IOProcs call this to update their IOState member. Also stops the IOProc if its state has been set to Stopping.
|
||||
// Returns true if it changes the state.
|
||||
static bool UpdateIOProcState(const char* __nullable callerName,
|
||||
|
@ -167,7 +179,6 @@ private:
|
|||
semaphore_t mOutputDeviceIOProcSemaphore { SEMAPHORE_NULL };
|
||||
|
||||
bool mActive = false;
|
||||
|
||||
bool mPlayingThrough = false;
|
||||
|
||||
UInt64 mLastNotifiedIOStoppedOnBGMDevice;
|
||||
|
|
|
@ -22,6 +22,7 @@
|
|||
|
||||
// Self Include
|
||||
#import "BGMXPCListener.h"
|
||||
#import "BGMPlayThrough.h" // For kDeviceNotStarting.
|
||||
|
||||
|
||||
#pragma clang assume_nonnull begin
|
||||
|
@ -181,11 +182,12 @@
|
|||
try {
|
||||
err = [audioDevices waitForOutputDeviceToStart];
|
||||
} catch (CAException e) {
|
||||
DebugMsg("BGMXPCListener::waitForOutputDeviceToStartWithReply: Caught CAException (%d). Replying kBGMXPC_HardwareError.",
|
||||
// waitForOutputDeviceToStart should never throw a CAException, but check anyway in case we change that at some point.
|
||||
LogError("BGMXPCListener::waitForOutputDeviceToStartWithReply: Caught CAException (%d). Replying kBGMXPC_HardwareError.",
|
||||
e.GetError());
|
||||
err = kBGMXPC_HardwareError;
|
||||
} catch (...) {
|
||||
DebugMsg("BGMXPCListener::waitForOutputDeviceToStartWithReply: Caught unknown exception. Replying kBGMXPC_InternalError.");
|
||||
LogError("BGMXPCListener::waitForOutputDeviceToStartWithReply: Caught unknown exception. Replying kBGMXPC_InternalError.");
|
||||
err = kBGMXPC_InternalError;
|
||||
#if DEBUG
|
||||
throw;
|
||||
|
@ -208,6 +210,12 @@
|
|||
err = kBGMXPC_HardwareError;
|
||||
break;
|
||||
|
||||
case BGMPlayThrough::kDeviceNotStarting:
|
||||
// We have to send a more specific error in this case because BGMDevice handles this case differently.
|
||||
description = @"The output device is not starting.";
|
||||
err = kBGMXPC_HardwareNotStartingError;
|
||||
break;
|
||||
|
||||
default:
|
||||
description = @"Unknown error while waiting for the output device.";
|
||||
err = kBGMXPC_InternalError;
|
||||
|
|
|
@ -67,8 +67,8 @@ void LogError(const char *fmt, ...)
|
|||
{
|
||||
va_list args;
|
||||
va_start(args, fmt);
|
||||
// BGM edit: vprintf leaves args in an undefined state, which can cause a crash
|
||||
// vsyslog. Original code commented out below.
|
||||
// BGM edit: vprintf leaves args in an undefined state, which can cause a crash in
|
||||
// vsyslog. Also added CADebuggerStop(). Original code commented out below.
|
||||
//#if DEBUG
|
||||
// vprintf(fmt, args);
|
||||
//#endif
|
||||
|
@ -76,9 +76,14 @@ void LogError(const char *fmt, ...)
|
|||
// vsyslog(LOG_ERR, fmt, args);
|
||||
//#endif
|
||||
#if (DEBUG || !TARGET_API_MAC_OSX) && !CoreAudio_UseSysLog
|
||||
printf("[ERROR] ");
|
||||
vprintf(fmt, args);
|
||||
printf("\n");
|
||||
#else
|
||||
vsyslog(LOG_ERR, fmt, args);
|
||||
#endif
|
||||
#if DEBUG
|
||||
CADebuggerStop();
|
||||
#endif
|
||||
// BGM edit end
|
||||
va_end(args);
|
||||
|
@ -88,8 +93,8 @@ void LogWarning(const char *fmt, ...)
|
|||
{
|
||||
va_list args;
|
||||
va_start(args, fmt);
|
||||
// BGM edit: vprintf leaves args in an undefined state, which can cause a crash
|
||||
// vsyslog. Original code commented out below.
|
||||
// BGM edit: vprintf leaves args in an undefined state, which can cause a crash in
|
||||
// vsyslog. Also added CADebuggerStop(). Original code commented out below.
|
||||
//#if DEBUG
|
||||
// vprintf(fmt, args);
|
||||
//#endif
|
||||
|
@ -97,9 +102,14 @@ void LogWarning(const char *fmt, ...)
|
|||
// vsyslog(LOG_WARNING, fmt, args);
|
||||
//#endif
|
||||
#if (DEBUG || !TARGET_API_MAC_OSX) && !CoreAudio_UseSysLog
|
||||
printf("[WARNING] ");
|
||||
vprintf(fmt, args);
|
||||
printf("\n");
|
||||
#else
|
||||
vsyslog(LOG_WARNING, fmt, args);
|
||||
#endif
|
||||
#if DEBUG
|
||||
//CADebuggerStop(); // TODO: Add a toggle for this to the project file (under "Preprocessor Macros"). Default to off.
|
||||
#endif
|
||||
// BGM edit end
|
||||
va_end(args);
|
||||
|
|
|
@ -17,7 +17,7 @@
|
|||
// BGM_Device.cpp
|
||||
// BGMDriver
|
||||
//
|
||||
// Copyright © 2016 Kyle Neideck
|
||||
// Copyright © 2016, 2017 Kyle Neideck
|
||||
// Copyright © 2016 Josh Junon
|
||||
// Portions copyright (C) 2013 Apple Inc. All Rights Reserved.
|
||||
//
|
||||
|
@ -25,6 +25,8 @@
|
|||
// NullAudio.c sample code (found in the same sample project).
|
||||
// https://developer.apple.com/library/mac/samplecode/AudioDriverExamples
|
||||
//
|
||||
// TODO: This class is now almost 2500 lines long.
|
||||
//
|
||||
|
||||
// Self Include
|
||||
#include "BGM_Device.h"
|
||||
|
@ -1869,20 +1871,28 @@ void BGM_Device::StartIO(UInt32 inClientID)
|
|||
{
|
||||
UInt64 theXPCError = WaitForBGMAppToStartOutputDevice();
|
||||
|
||||
if(theXPCError == kBGMXPC_Success)
|
||||
switch(theXPCError)
|
||||
{
|
||||
DebugMsg("BGM_Device::StartIO: Ready for IO.");
|
||||
}
|
||||
else if(theXPCError == kBGMXPC_MessageFailure)
|
||||
{
|
||||
// This most likely means BGMXPCHelper isn't installed or has crashed. IO will probably still work,
|
||||
// but we may drop frames while the audio hardware starts up.
|
||||
DebugMsg("BGM_Device::StartIO: Couldn't reach BGMApp via XPC. Attempting to start IO anyway.");
|
||||
}
|
||||
else
|
||||
{
|
||||
DebugMsg("BGM_Device::StartIO: BGMApp failed to start the output device. theXPCError=%llu", theXPCError);
|
||||
Throw(CAException(kAudioHardwareUnspecifiedError));
|
||||
case kBGMXPC_Success:
|
||||
DebugMsg("BGM_Device::StartIO: Ready for IO.");
|
||||
break;
|
||||
|
||||
case kBGMXPC_MessageFailure:
|
||||
// This most likely means BGMXPCHelper isn't installed or has crashed. IO will probably still work,
|
||||
// but we may drop frames while the audio hardware starts up.
|
||||
LogError("BGM_Device::StartIO: Couldn't reach BGMApp via XPC. Attempting to start IO anyway.");
|
||||
break;
|
||||
|
||||
case kBGMXPC_HardwareNotStartingError:
|
||||
// This can (and might always) happen when the user changes output device in BGMApp while IO is running.
|
||||
// See BGMAudioDeviceManager::waitForOutputDeviceToStart and BGMPlayThrough::WaitForOutputDeviceToStart.
|
||||
LogWarning("BGM_Device::StartIO: BGMApp hadn't been told to start IO, so BGMDriver has to return early "
|
||||
"from StartIO. Attempting to start IO anyway.");
|
||||
break;
|
||||
|
||||
default:
|
||||
LogError("BGM_Device::StartIO: BGMApp failed to start the output device. theXPCError=%llu", theXPCError);
|
||||
Throw(CAException(kAudioHardwareNotRunningError));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -66,13 +66,26 @@ void DebugPrint(const char *fmt, ...)
|
|||
void LogError(const char *fmt, ...)
|
||||
{
|
||||
va_list args;
|
||||
va_start(args, fmt);
|
||||
va_start(args, fmt);
|
||||
// BGM edit: vprintf leaves args in an undefined state, which can cause a crash in
|
||||
// vsyslog. Also added CADebuggerStop(). Original code commented out below.
|
||||
//#if DEBUG
|
||||
// vprintf(fmt, args);
|
||||
//#endif
|
||||
//#if TARGET_API_MAC_OSX
|
||||
// vsyslog(LOG_ERR, fmt, args);
|
||||
//#endif
|
||||
#if (DEBUG || !TARGET_API_MAC_OSX) && !CoreAudio_UseSysLog
|
||||
printf("[ERROR] ");
|
||||
vprintf(fmt, args);
|
||||
printf("\n");
|
||||
#else
|
||||
vsyslog(LOG_ERR, fmt, args);
|
||||
#endif
|
||||
#if DEBUG
|
||||
vprintf(fmt, args);
|
||||
#endif
|
||||
#if TARGET_API_MAC_OSX
|
||||
vsyslog(LOG_ERR, fmt, args);
|
||||
CADebuggerStop();
|
||||
#endif
|
||||
// BGM edit end
|
||||
va_end(args);
|
||||
}
|
||||
|
||||
|
@ -80,11 +93,24 @@ void LogWarning(const char *fmt, ...)
|
|||
{
|
||||
va_list args;
|
||||
va_start(args, fmt);
|
||||
// BGM edit: vprintf leaves args in an undefined state, which can cause a crash in
|
||||
// vsyslog. Also added CADebuggerStop(). Original code commented out below.
|
||||
//#if DEBUG
|
||||
// vprintf(fmt, args);
|
||||
//#endif
|
||||
//#if TARGET_API_MAC_OSX
|
||||
// vsyslog(LOG_WARNING, fmt, args);
|
||||
//#endif
|
||||
#if (DEBUG || !TARGET_API_MAC_OSX) && !CoreAudio_UseSysLog
|
||||
printf("[WARNING] ");
|
||||
vprintf(fmt, args);
|
||||
printf("\n");
|
||||
#else
|
||||
vsyslog(LOG_WARNING, fmt, args);
|
||||
#endif
|
||||
#if DEBUG
|
||||
vprintf(fmt, args);
|
||||
#endif
|
||||
#if TARGET_API_MAC_OSX
|
||||
vsyslog(LOG_WARNING, fmt, args);
|
||||
//CADebuggerStop(); // TODO: Add a toggle for this to the project file (under "Preprocessor Macros"). Default to off.
|
||||
#endif
|
||||
// BGM edit end
|
||||
va_end(args);
|
||||
}
|
||||
|
|
|
@ -194,8 +194,9 @@
|
|||
#define DebugMessageN8(msg, N1, N2, N3, N4, N5, N6, N7, N8) DebugMsg(msg, N1, N2, N3, N4, N5, N6, N7, N8)
|
||||
#define DebugMessageN9(msg, N1, N2, N3, N4, N5, N6, N7, N8, N9) DebugMsg(msg, N1, N2, N3, N4, N5, N6, N7, N8, N9)
|
||||
|
||||
void LogError(const char *fmt, ...); // writes to syslog (and stderr if debugging)
|
||||
void LogWarning(const char *fmt, ...); // writes to syslog (and stderr if debugging)
|
||||
// BGM edit: Added __printflike.
|
||||
void LogError(const char *fmt, ...) __printflike(1, 2); // writes to syslog (and stderr if debugging)
|
||||
void LogWarning(const char *fmt, ...) __printflike(1, 2); // writes to syslog (and stderr if debugging)
|
||||
|
||||
#define NO_ACTION (void)0
|
||||
|
||||
|
|
|
@ -163,6 +163,7 @@ enum {
|
|||
kBGMXPC_Timeout,
|
||||
kBGMXPC_BGMAppStateError,
|
||||
kBGMXPC_HardwareError,
|
||||
kBGMXPC_HardwareNotStartingError,
|
||||
kBGMXPC_InternalError
|
||||
};
|
||||
|
||||
|
|
Loading…
Reference in a new issue