// This file is part of Background Music. // // Background Music is free software: you can redistribute it and/or // modify it under the terms of the GNU General Public License as // published by the Free Software Foundation, either version 2 of the // License, or (at your option) any later version. // // Background Music is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Background Music. If not, see . // // VCPlayThrough.cpp // VCApp // // Copyright © 2016, 2017, 2020 Kyle Neideck // // Self Include #include "VCPlayThrough.h" // Local Includes #include "VC_Types.h" #include "VC_Utils.h" // PublicUtility Includes #include "CAHALAudioSystemObject.h" #include "CAPropertyAddress.h" // STL Includes #include // For std::max // System Includes #include #include #include // The number of IO cycles (roughly) to wait for our IOProcs to stop themselves before assuming something // went wrong. If that happens, we try to stop them from a non-IO thread and continue anyway. static const UInt32 kStopIOProcTimeoutInIOCycles = 600; #pragma mark Construction/Destruction VCPlayThrough::VCPlayThrough(VCAudioDevice inInputDevice, VCAudioDevice inOutputDevice) : mInputDevice(inInputDevice), mOutputDevice(inOutputDevice) { Init(inInputDevice, inOutputDevice); } VCPlayThrough::~VCPlayThrough() { CAMutex::Locker stateLocker(mStateMutex); VCLogAndSwallowExceptionsMsg("VCPlayThrough::~VCPlayThrough", "Deactivate", [&]() { Deactivate(); }); // If one of the IOProcs failed to stop, CoreAudio could (at least in theory) still call it // after this point. This isn't a solution, but calling DeallocateBuffer instead of letting it // deallocate itself should at least make the error less likely to cause a segfault, since // DeallocateBuffer takes the buffer locks and sets mBuffer to null. // // TODO: It probably wouldn't be too hard to fix this properly by giving the IOProcs weak refs // to the VCPlayThrough object instead of raw pointers. DeallocateBuffer(); if(mOutputDeviceIOProcSemaphore != SEMAPHORE_NULL) { kern_return_t theError = semaphore_destroy(mach_task_self(), mOutputDeviceIOProcSemaphore); VC_Utils::LogIfMachError("VCPlayThrough::~VCPlayThrough", "semaphore_destroy", theError); } } void VCPlayThrough::Init(VCAudioDevice inInputDevice, VCAudioDevice inOutputDevice) { VCAssert(mInputDeviceIOProcState.is_lock_free(), "VCPlayThrough::VCPlayThrough: !mInputDeviceIOProcState.is_lock_free()"); VCAssert(mOutputDeviceIOProcState.is_lock_free(), "VCPlayThrough::VCPlayThrough: !mOutputDeviceIOProcState.is_lock_free()"); VCAssert(!mActive, "VCPlayThrough::VCPlayThrough: Can't init while active."); mInputDevice = inInputDevice; mOutputDevice = inOutputDevice; AllocateBuffer(); try { // 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); VC_Utils::ThrowIfMachError("VCPlayThrough::VCPlayThrough", "semaphore_create", theError); ThrowIf(mOutputDeviceIOProcSemaphore == SEMAPHORE_NULL, CAException(kAudioHardwareUnspecifiedError), "VCPlayThrough::VCPlayThrough: Could not create semaphore"); } } catch (...) { // Clean up. DeallocateBuffer(); throw; } } void VCPlayThrough::Activate() { CAMutex::Locker stateLocker(mStateMutex); if(!mActive) { DebugMsg("VCPlayThrough::Activate: Activating playthrough"); CreateIOProcIDs(); mActive = true; // TODO: This code (the next two blocks) should be in VCDeviceControlSync. // Set VCDevice's sample rate to match the output device. try { Float64 outputSampleRate = mOutputDevice.GetNominalSampleRate(); mInputDevice.SetNominalSampleRate(outputSampleRate); } catch (CAException e) { LogWarning("VCPlayThrough::Activate: Failed to sync device sample rates. Error: %d", e.GetError()); } // Set VCDevice's IO buffer size to match the output device. try { UInt32 outputBufferSize = mOutputDevice.GetIOBufferSize(); mInputDevice.SetIOBufferSize(outputBufferSize); } catch (CAException e) { LogWarning("VCPlayThrough::Activate: Failed to sync device buffer sizes. Error: %d", e.GetError()); } DebugMsg("VCPlayThrough::Activate: Registering for notifications from VCDevice."); mInputDevice.AddPropertyListener(CAPropertyAddress(kAudioDevicePropertyDeviceIsRunning), &VCPlayThrough::VCDeviceListenerProc, this); mInputDevice.AddPropertyListener(CAPropertyAddress(kAudioDeviceProcessorOverload), &VCPlayThrough::VCDeviceListenerProc, this); bool isVCDevice = true; CATry isVCDevice = mInputDevice.IsVCDeviceInstance(); CACatch if(isVCDevice) { mInputDevice.AddPropertyListener(kVCRunningSomewhereOtherThanVCAppAddress, &VCPlayThrough::VCDeviceListenerProc, this); } else { LogWarning("VCPlayThrough::Activate: Playthrough activated with an output device other " "than VCDevice. This hasn't been tested and is almost definitely a bug."); VCAssert(false, "VCPlayThrough::Activate: !mInputDevice.IsVCDeviceInstance()"); } } } void VCPlayThrough::Deactivate() { CAMutex::Locker stateLocker(mStateMutex); if(mActive) { DebugMsg("VCPlayThrough::Deactivate: Deactivating playthrough"); bool inputDeviceIsVCDevice = true; CATry inputDeviceIsVCDevice = mInputDevice.IsVCDeviceInstance(); CACatch // Unregister notification listeners. if(inputDeviceIsVCDevice) { // There's not much we can do if these calls throw. The docs for AudioObjectRemovePropertyListener // just say that means it failed. VCLogAndSwallowExceptions("VCPlayThrough::Deactivate", [&] { mInputDevice.RemovePropertyListener(CAPropertyAddress(kAudioDevicePropertyDeviceIsRunning), &VCPlayThrough::VCDeviceListenerProc, this); }); VCLogAndSwallowExceptions("VCPlayThrough::Deactivate", [&] { mInputDevice.RemovePropertyListener(CAPropertyAddress(kAudioDeviceProcessorOverload), &VCPlayThrough::VCDeviceListenerProc, this); }); VCLogAndSwallowExceptions("VCPlayThrough::Deactivate", [&] { mInputDevice.RemovePropertyListener(kVCRunningSomewhereOtherThanVCAppAddress, &VCPlayThrough::VCDeviceListenerProc, this); }); } VCLogAndSwallowExceptions("VCPlayThrough::Deactivate", [&] { Stop(); }); VCLogAndSwallowExceptions("VCPlayThrough::Deactivate", [&] { DestroyIOProcIDs(); }); mActive = false; } } void VCPlayThrough::AllocateBuffer() { // Allocate the ring buffer that will hold the data passing between the devices UInt32 numberStreams = 1; AudioStreamBasicDescription outputFormat[1]; mOutputDevice.GetCurrentVirtualFormats(false, numberStreams, outputFormat); if(numberStreams < 1) { Throw(CAException(kAudioHardwareUnsupportedOperationError)); } // Need to lock the buffer mutexes to make sure the IOProcs aren't accessing it. The order is // important here. We always lock them in the same order to prevent deadlocks. CAMutex::Locker lockerInput(mBufferInputMutex); CAMutex::Locker lockerOutput(mBufferOutputMutex); mBuffer = std::unique_ptr(new CARingBuffer); // The calculation for the size of the buffer is from Apple's CAPlayThrough.cpp sample code // // TODO: Test playthrough with hardware with more than 2 channels per frame, a sample (virtual) format other than // 32-bit floats and/or an IO buffer size other than 512 frames mBuffer->Allocate(outputFormat[0].mChannelsPerFrame, outputFormat[0].mBytesPerFrame, mOutputDevice.GetIOBufferSize() * 20); } void VCPlayThrough::DeallocateBuffer() { // Need to lock the buffer mutexes to make sure the IOProcs aren't accessing it. The order is // important here. We always lock them in the same order to prevent deadlocks. CAMutex::Locker lockerInput(mBufferInputMutex); CAMutex::Locker lockerOutput(mBufferOutputMutex); mBuffer = nullptr; // Note that the buffer's destructor will deallocate it. } void VCPlayThrough::CreateIOProcIDs() { CAMutex::Locker stateLocker(mStateMutex); VCAssert(!mPlayingThrough, "VCPlayThrough::CreateIOProcIDs: Tried to create IOProcs when playthrough was already running"); VCAssert(mInputDeviceIOProcID == nullptr, "VCPlayThrough::CreateIOProcIDs: mInputDeviceIOProcID must be destroyed first."); VCAssert(mOutputDeviceIOProcID == nullptr, "VCPlayThrough::CreateIOProcIDs: mOutputDeviceIOProcID must be destroyed first."); VCAssert(CheckIOProcsAreStopped(), "VCPlayThrough::CreateIOProcIDs: IOProcs not ready."); const bool inDeviceAlive = mInputDevice.IsAlive(); const bool outDeviceAlive = mOutputDevice.IsAlive(); if(inDeviceAlive && outDeviceAlive) { DebugMsg("VCPlayThrough::CreateIOProcIDs: Creating IOProcs"); try { mInputDeviceIOProcID = mInputDevice.CreateIOProcID(&VCPlayThrough::InputDeviceIOProc, this); } catch(CAException e) { LogWarning("VCPlayThrough::CreateIOProcIDs: Failed to create input IOProc ID. mInputDevice = %d", mInputDevice.GetObjectID()); throw; } try { mOutputDeviceIOProcID = mOutputDevice.CreateIOProcID(&VCPlayThrough::OutputDeviceIOProc, this); } catch(CAException e) { LogWarning("VCPlayThrough::CreateIOProcIDs: Failed to create output IOProc ID. mOutputDevice = %d", mOutputDevice.GetObjectID()); DestroyIOProcIDs(); // Clean up. throw; } if(mInputDeviceIOProcID == nullptr || mOutputDeviceIOProcID == nullptr) { // Should never happen if CAHALAudioDevice::CreateIOProcID didn't throw. LogError("VCPlayThrough::CreateIOProcIDs: Null IOProc ID returned by CreateIOProcID"); Throw(CAException(kAudioHardwareIllegalOperationError)); } // TODO: Try using SetIOCycleUsage to reduce latency? Our IOProcs don't really do anything except copy a small // buffer. According to this, Jack OS X considered it: // https://lists.apple.com/archives/coreaudio-api/2008/Mar/msg00043.html but from a quick look at their // code, I don't think they ended up using it. // mInputDevice->SetIOCycleUsage(0.01f); // mOutputDevice->SetIOCycleUsage(0.01f); } else { LogWarning("VCPlayThrough::CreateIOProcIDs: Failed to create IOProcs.%s%s", (inDeviceAlive ? "" : " Input device not alive."), (outDeviceAlive ? "" : " Output device not alive.")); Throw(CAException(kAudioHardwareIllegalOperationError)); } } void VCPlayThrough::DestroyIOProcIDs() { CAMutex::Locker stateLocker(mStateMutex); // In release builds, we still try to destroy the IDs if the IOProcs are running, hoping they just haven't been // stopped quite yet. The docs for AudioDeviceDestroyIOProcID don't say not to do that, but it could cause races // if one really is still running so it isn't ideal. VCAssert(CheckIOProcsAreStopped(), "VCPlayThrough::DestroyIOProcIDs: IOProcs not ready."); DebugMsg("VCPlayThrough::DestroyIOProcIDs: Destroying IOProcs"); auto destroy = [](VCAudioDevice& device, const char* deviceName, AudioDeviceIOProcID& ioProcID) { #if !DEBUG #pragma unused (deviceName) #endif if(ioProcID != nullptr) { try { device.DestroyIOProcID(ioProcID); } catch(CAException e) { if((e.GetError() == kAudioHardwareBadDeviceError) || (e.GetError() == kAudioHardwareBadObjectError)) { // This means the IOProc IDs will have already been destroyed, so there's nothing to do. DebugMsg("VCPlayThrough::DestroyIOProcIDs: Didn't destroy IOProc ID for %s device because " "it's not connected anymore. deviceID = %d", deviceName, device.GetObjectID()); } else { ioProcID = nullptr; throw; } } ioProcID = nullptr; } }; destroy(mInputDevice, "input", mInputDeviceIOProcID); destroy(mOutputDevice, "output", mOutputDeviceIOProcID); } bool VCPlayThrough::CheckIOProcsAreStopped() const noexcept { bool statesOK = true; if(mInputDeviceIOProcState != IOState::Stopped) { LogWarning("VCPlayThrough::CheckIOProcsAreStopped: Input IOProc not stopped. mInputDeviceIOProcState = %d", mInputDeviceIOProcState.load()); statesOK = false; } if(mOutputDeviceIOProcState != IOState::Stopped) { LogWarning("VCPlayThrough::CheckIOProcsAreStopped: Output IOProc not stopped. mOutputDeviceIOProcState = %d", mOutputDeviceIOProcState.load()); statesOK = false; } return statesOK; } void VCPlayThrough::SetDevices(const VCAudioDevice* __nullable inInputDevice, const VCAudioDevice* __nullable inOutputDevice) { CAMutex::Locker stateLocker(mStateMutex); bool wasActive = mActive; bool wasPlayingThrough = mPlayingThrough; if(wasPlayingThrough) { VCAssert(wasActive, "VCPlayThrough::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 VCPlayThrough::Start() { CAMutex::Locker stateLocker(mStateMutex); if(mPlayingThrough) { DebugMsg("VCPlayThrough::Start: Already started/starting."); if(mOutputDeviceIOProcState == IOState::Running) { ReleaseThreadsWaitingForOutputToStart(); } return; } if(!mInputDevice.IsAlive() || !mOutputDevice.IsAlive()) { LogError("VCPlayThrough::Start: %s %s", mInputDevice.IsAlive() ? "" : "!mInputDevice", mOutputDevice.IsAlive() ? "" : "!mOutputDevice"); ReleaseThreadsWaitingForOutputToStart(); throw CAException(kAudioHardwareBadDeviceError); } // Set up IOProcs and listeners if they haven't been already. Activate(); VCAssert((mInputDeviceIOProcID != nullptr) && (mOutputDeviceIOProcID != nullptr), "VCPlayThrough::Start: Null IOProc ID"); if((mInputDeviceIOProcState != IOState::Stopped) || (mOutputDeviceIOProcState != IOState::Stopped)) { LogWarning("VCPlayThrough::Start: IOProc(s) not ready. Trying to start anyway. %s%d %s%d", "mInputDeviceIOProcState = ", mInputDeviceIOProcState.load(), "mOutputDeviceIOProcState = ", mOutputDeviceIOProcState.load()); } DebugMsg("VCPlayThrough::Start: Starting playthrough"); // Start our IOProcs. try { mInputDeviceIOProcState = IOState::Starting; mInputDevice.StartIOProc(mInputDeviceIOProcID); mOutputDeviceIOProcState = IOState::Starting; mOutputDevice.StartIOProc(mOutputDeviceIOProcID); } catch(CAException e) { ReleaseThreadsWaitingForOutputToStart(); // Log an error message. OSStatus err = e.GetError(); char err4CC[5] = CA4CCToCString(err); LogError("VCPlayThrough::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 CATry mOutputDevice.StopIOProc(mOutputDeviceIOProcID); CACatch mInputDeviceIOProcState = IOState::Stopped; mOutputDeviceIOProcState = IOState::Stopped; throw; } mPlayingThrough = true; } OSStatus VCPlayThrough::WaitForOutputDeviceToStart() noexcept { // Check for errors. // // Technically we should take the state mutex here, but that could cause deadlocks because // VC_Device::StartIO (in VCDriver) blocks on this function (via XPC). Other VCPlayThrough // functions make requests to VCDriver while holding the state mutex, usually to get/set // properties, but the HAL will block those requests until VC_Device::StartIO returns. try { if(!mActive) { LogError("VCPlayThrough::WaitForOutputDeviceToStart: !mActive"); return kAudioHardwareNotRunningError; } if(!mOutputDevice.IsAlive()) { LogError("VCPlayThrough::WaitForOutputDeviceToStart: Device not alive"); return kAudioHardwareBadDeviceError; } } catch(const CAException& e) { VCLogException(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("VCPlayThrough::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. // // This does mean that we won't have any data the first time our IOProc is called, but I // don't know any way to wait until just before that point. (The device's IsRunning property // 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 miss the signal from the IOProc because of a // spurious wake up, so it might actually be necessary.) DebugMsg("VCPlayThrough::WaitForOutputDeviceToStart: Waiting."); kern_return_t theError; IOState state; UInt64 waitedNsec = 0; mach_timebase_info_data_t info; mach_timebase_info(&info); do { VCAssert(mOutputDeviceIOProcSemaphore != SEMAPHORE_NULL, "VCPlayThrough::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. waitedNsec = (mach_absolute_time() - startedAt) * info.numer / info.denom; state = mOutputDeviceIOProcState; } while((theError != KERN_SUCCESS) && // Signalled from the IOProc. (state == IOState::Starting) && // IO state changed. (waitedNsec < kStartIOTimeoutNsec)); // Timed out. if(VCDebugLoggingIsEnabled()) { UInt64 startedBy = mach_absolute_time(); struct mach_timebase_info baseInfo = { 0, 0 }; mach_timebase_info(&baseInfo); UInt64 base = baseInfo.numer / baseInfo.denom; DebugMsg("VCPlayThrough::WaitForOutputDeviceToStart: Started %f ms after notification, %f " "ms after entering WaitForOutputDeviceToStart.", static_cast(startedBy - mToldOutputDeviceToStartAt) * base / NSEC_PER_MSEC, static_cast(startedBy - startedAt) * base / NSEC_PER_MSEC); } // Figure out which error code to return. switch (theError) { case KERN_SUCCESS: // Signalled from the IOProc. return kAudioHardwareNoError; // IO state changed or we timed out after case KERN_OPERATION_TIMED_OUT: // - semaphore_timedwait timed out, or case KERN_ABORTED: // - a spurious wake-up. return (state == IOState::Running) ? kAudioHardwareNoError : kAudioHardwareNotRunningError; default: VC_Utils::LogIfMachError("VCPlayThrough::WaitForOutputDeviceToStart", "semaphore_timedwait", theError); return kAudioHardwareUnspecifiedError; } } // Release any threads waiting for the output device to start. This function doesn't take mStateMutex // because it gets called on the IO thread, which is realtime priority. void VCPlayThrough::ReleaseThreadsWaitingForOutputToStart() { if(mActive) { semaphore_t semaphore = mOutputDeviceIOProcSemaphore; if(semaphore != SEMAPHORE_NULL) { mRTLogger.LogReleasingWaitingThreads(); kern_return_t theError = semaphore_signal_all(semaphore); mRTLogger.LogIfMachError_ReleaseWaitingThreadsSignal(theError); } } } OSStatus VCPlayThrough::Stop() { CAMutex::Locker stateLocker(mStateMutex); // TODO: Tell the waiting threads what happened so they can return an error? ReleaseThreadsWaitingForOutputToStart(); if(mActive && mPlayingThrough) { DebugMsg("VCPlayThrough::Stop: Stopping playthrough"); bool inputDeviceAlive = false; bool outputDeviceAlive = false; CATry inputDeviceAlive = CAHALAudioObject::ObjectExists(mInputDevice) && mInputDevice.IsAlive(); CACatch CATry outputDeviceAlive = CAHALAudioObject::ObjectExists(mOutputDevice) && mOutputDevice.IsAlive(); CACatch mInputDeviceIOProcState = inputDeviceAlive ? IOState::Stopping : IOState::Stopped; mOutputDeviceIOProcState = outputDeviceAlive ? IOState::Stopping : IOState::Stopped; // Wait for the IOProcs to stop themselves. This is so the IOProcs don't get called after the VCPlayThrough instance // (pointed to by the client data they get from the HAL) is deallocated. // // From Jeff Moore on the Core Audio mailing list: // Note that there is no guarantee about how many times your IOProc might get called after AudioDeviceStop() returns // 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; VC_Utils::LogAndSwallowExceptions(VCDbgArgs, [&]() { Float64 expectedInputCycleNs = 0; if(inputDeviceAlive) { expectedInputCycleNs = mInputDevice.GetIOBufferSize() * (1 / mInputDevice.GetNominalSampleRate()) * NSEC_PER_SEC; } Float64 expectedOutputCycleNs = 0; if(outputDeviceAlive) { expectedOutputCycleNs = mOutputDevice.GetIOBufferSize() * (1 / mOutputDevice.GetNominalSampleRate()) * NSEC_PER_SEC; } UInt64 expectedMaxCycleNs = static_cast(std::max(expectedInputCycleNs, expectedOutputCycleNs)); while((mInputDeviceIOProcState == IOState::Stopping || mOutputDeviceIOProcState == IOState::Stopping) && (totalWaitNs < kStopIOProcTimeoutInIOCycles * 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) { LogError("VCPlayThrough::Stop: The input IOProc didn't stop itself in time. Stopping " "it from outside of the IO thread."); VCLogUnexpectedExceptions("VCPlayThrough::Stop", [&]() { mInputDevice.StopIOProc(mInputDeviceIOProcID); }); mInputDeviceIOProcState = IOState::Stopped; } if(mOutputDeviceIOProcState == IOState::Stopping && mOutputDeviceIOProcID != nullptr) { LogError("VCPlayThrough::Stop: The output IOProc didn't stop itself in time. Stopping " "it from outside of the IO thread."); VCLogUnexpectedExceptions("VCPlayThrough::Stop", [&]() { mOutputDevice.StopIOProc(mOutputDeviceIOProcID); }); mOutputDeviceIOProcState = IOState::Stopped; } mPlayingThrough = false; } mFirstInputSampleTime = -1; mLastInputSampleTime = -1; mLastOutputSampleTime = -1; return noErr; // TODO: Why does this return anything and why always noErr? } void VCPlayThrough::StopIfIdle() { // To save CPU time, we stop playthrough when no clients are doing IO. This should reduce the coreaudiod and VCApp // processes' idle CPU use to virtually none. If this isn't working for you, a client might be running IO without // being audible. VLC does that when you have a file paused, for example. CAMutex::Locker stateLocker(mStateMutex); VCAssert(mInputDevice.IsVCDeviceInstance(), "VCDevice not set as input device. StopIfIdle can't tell if other devices are idle."); if(!IsRunningSomewhereOtherThanVCApp(mInputDevice)) { mLastNotifiedIOStoppedOnVCDevice = mach_absolute_time(); // Wait a bit before stopping playthrough. // // This keeps us from starting and stopping IO too rapidly, which wastes CPU, and gives VCDriver time to update // kAudioDeviceCustomPropertyDeviceAudibleState, which it can only do while IO is running. (The wait duration is // more or less arbitrary, except that it has to be longer than kDeviceAudibleStateMinChangedFramesForUpdate.) // 1 / sample rate = seconds per frame Float64 nsecPerFrame = (1.0 / mInputDevice.GetNominalSampleRate()) * NSEC_PER_SEC; UInt64 waitNsec = static_cast(20 * 4096 * nsecPerFrame); UInt64 queuedAt = mLastNotifiedIOStoppedOnVCDevice; DebugMsg("VCPlayThrough::StopIfIdle: Will dispatch stop-if-idle block in %llu ns. %s%llu", waitNsec, "queuedAt=", queuedAt); dispatch_after(dispatch_time(DISPATCH_TIME_NOW, waitNsec), dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ // Check the VCPlayThrough instance hasn't been destructed since it queued this block if(mActive) { // The "2" is just to avoid shadowing the other locker CAMutex::Locker stateLocker2(mStateMutex); // Don't stop playthrough if IO has started running again or if // kAudioDeviceCustomPropertyDeviceIsRunningSomewhereOtherThanVCApp has changed since // this block was queued if(mPlayingThrough && !IsRunningSomewhereOtherThanVCApp(mInputDevice) && queuedAt == mLastNotifiedIOStoppedOnVCDevice) { DebugMsg("VCPlayThrough::StopIfIdle: VCDevice is only running IO for VCApp. " "Stopping playthrough."); Stop(); } } }); } } #pragma mark VCDevice Listener // TODO: Listen for changes to the sample rate and IO buffer size of the output device and update the input device to match // static OSStatus VCPlayThrough::VCDeviceListenerProc(AudioObjectID inObjectID, UInt32 inNumberAddresses, const AudioObjectPropertyAddress* __nonnull inAddresses, void* __nullable inClientData) { // refCon (reference context) is the instance that registered the listener proc VCPlayThrough* refCon = static_cast(inClientData); // If the input device isn't VCDevice, this listener proc shouldn't be registered ThrowIf(inObjectID != refCon->mInputDevice.GetObjectID(), CAException(kAudioHardwareBadObjectError), "VCPlayThrough::VCDeviceListenerProc: notified about audio object other than VCDevice"); for(int i = 0; i < inNumberAddresses; i++) { switch(inAddresses[i].mSelector) { case kAudioDeviceProcessorOverload: // These warnings are common when you use the UI if you're running a debug build or have "Debug executable" // checked. You shouldn't be seeing them otherwise. DebugMsg("VCPlayThrough::VCDeviceListenerProc: WARNING! Got kAudioDeviceProcessorOverload notification"); LogWarning("Background Music: CPU overload reported\n"); break; // Start playthrough when a client starts IO on VCDevice and stop when VCApp (i.e. playthrough itself) is // the only client left doing IO. // // These cases are dispatched to avoid causing deadlocks by triggering one of the following notifications in // the process of handling one. Deadlocks could happen if these were handled synchronously when: // - the first VCDeviceListenerProc call takes the state mutex, then requests some data from the HAL and // waits for it to return, // - the request triggers the HAL to send notifications, which it sends on a different thread, // - the HAL waits for the second VCDeviceListenerProc call to return before it returns the data // requested by the first VCDeviceListenerProc call, and // - the second VCDeviceListenerProc call waits for the first to unlock the state mutex. case kAudioDevicePropertyDeviceIsRunning: // Received on the IO thread before our IOProc is called HandleVCDeviceIsRunning(refCon); break; case kAudioDeviceCustomPropertyDeviceIsRunningSomewhereOtherThanVCApp: HandleVCDeviceIsRunningSomewhereOtherThanVCApp(refCon); break; default: // We might get properties we didn't ask for, so we just ignore them. break; } } // From AudioHardware.h: "The return value is currently unused and should always be 0." return 0; } // static void VCPlayThrough::HandleVCDeviceIsRunning(VCPlayThrough* refCon) { DebugMsg("VCPlayThrough::HandleVCDeviceIsRunning: Got notification"); // This is dispatched because it can block and // - we might be on a real-time thread, or // - VCXPCListener::startPlayThroughSyncWithReply might get called on the same thread just // before this and time out waiting for this to run. // // TODO: We should find a way to do this without dispatching because dispatching isn't actually // real-time safe. dispatch_async(VCGetDispatchQueue_PriorityUserInteractive(), ^{ if(refCon->mActive) { CAMutex::Locker stateLocker(refCon->mStateMutex); // Set to true initially because if we fail to get this property from VCDevice we want to // try to start playthrough anyway. bool isRunningSomewhereOtherThanVCApp = true; VCLogAndSwallowExceptions("HandleVCDeviceIsRunning", [&]() { // IsRunning doesn't always return true when IO is starting. Using // RunningSomewhereOtherThanVCApp instead seems to be working so far. isRunningSomewhereOtherThanVCApp = IsRunningSomewhereOtherThanVCApp(refCon->mInputDevice); }); DebugMsg("VCPlayThrough::HandleVCDeviceIsRunning: " "VCDevice is %srunning somewhere other than VCApp", isRunningSomewhereOtherThanVCApp ? "" : "not "); if(isRunningSomewhereOtherThanVCApp) { refCon->mToldOutputDeviceToStartAt = mach_absolute_time(); // TODO: Handle expected exceptions (mostly CAExceptions from PublicUtility classes) in Start. // For any that can't be handled sensibly in Start, catch them here and retry a few // times (with a very short delay) before handling them by showing an unobtrusive error // message or something. Then try a different device or just set the system device back // to the real device. VCLogAndSwallowExceptions("HandleVCDeviceIsRunning", [&refCon]() { refCon->Start(); }); } } }); } // static void VCPlayThrough::HandleVCDeviceIsRunningSomewhereOtherThanVCApp(VCPlayThrough* refCon) { DebugMsg("VCPlayThrough::HandleVCDeviceIsRunningSomewhereOtherThanVCApp: Got notification"); // These notifications don't need to be handled quickly, so we can always dispatch. dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ // TODO: Handle expected exceptions (mostly CAExceptions from PublicUtility classes) in StopIfIdle. VCLogUnexpectedExceptions("HandleVCDeviceIsRunningSomewhereOtherThanVCApp", [&refCon]() { if(refCon->mActive) { refCon->StopIfIdle(); } }); }); } // static bool VCPlayThrough::IsRunningSomewhereOtherThanVCApp(const VCAudioDevice& inVCDevice) { auto type = inVCDevice.GetPropertyData_CFType(kVCRunningSomewhereOtherThanVCAppAddress); return type && CFBooleanGetValue(static_cast(type)); } #pragma mark IOProcs // Note that the IOProcs will very likely not run on the same thread and that they intentionally // only lock mutexes around their use of mBuffer. // static OSStatus VCPlayThrough::InputDeviceIOProc(AudioObjectID inDevice, const AudioTimeStamp* inNow, const AudioBufferList* inInputData, const AudioTimeStamp* inInputTime, AudioBufferList* outOutputData, const AudioTimeStamp* inOutputTime, void* __nullable inClientData) { #pragma unused (inDevice, inNow, outOutputData, inOutputTime) // refCon (reference context) is the instance that created the IOProc VCPlayThrough* const refCon = static_cast(inClientData); IOState state; UpdateIOProcState("InputDeviceIOProc", refCon->mRTLogger, refCon->mInputDeviceIOProcState, refCon->mInputDeviceIOProcID, refCon->mInputDevice, state); if(state == IOState::Stopped || state == IOState::Stopping) { // Return early, since we just asked to stop. (Or something really weird is going on.) return noErr; } VCAssert(state == IOState::Running, "VCPlayThrough::InputDeviceIOProc: Unexpected state"); if(refCon->mFirstInputSampleTime == -1) { refCon->mFirstInputSampleTime = inInputTime->mSampleTime; } UInt32 framesToStore = inInputData->mBuffers[0].mDataByteSize / (SizeOf32(Float32) * 2); // See the comments in OutputDeviceIOProc where it locks mBufferOutputMutex. CAMutex::Tryer tryer(refCon->mBufferInputMutex); // Disable a warning about accessing mBuffer without holding both mBufferInputMutex and // mBufferOutputMutex. Explained further in OutputDeviceIOProc. #pragma clang diagnostic push #pragma clang diagnostic ignored "-Wthread-safety" if(tryer.HasLock() && refCon->mBuffer) { CARingBufferError err = refCon->mBuffer->Store(inInputData, framesToStore, static_cast( inInputTime->mSampleTime)); #pragma clang diagnostic pop refCon->mRTLogger.LogIfRingBufferError_Store(err); refCon->mLastInputSampleTime = inInputTime->mSampleTime; } else { refCon->mRTLogger.LogRingBufferUnavailable("InputDeviceIOProc", tryer.HasLock()); } return noErr; } // static OSStatus VCPlayThrough::OutputDeviceIOProc(AudioObjectID inDevice, const AudioTimeStamp* inNow, const AudioBufferList* inInputData, const AudioTimeStamp* inInputTime, AudioBufferList* outOutputData, const AudioTimeStamp* inOutputTime, void* __nullable inClientData) { #pragma unused (inDevice, inNow, inInputData, inInputTime) // refCon (reference context) is the instance that created the IOProc VCPlayThrough* const refCon = static_cast(inClientData); IOState state; const bool didChangeState = UpdateIOProcState("OutputDeviceIOProc", refCon->mRTLogger, refCon->mOutputDeviceIOProcState, refCon->mOutputDeviceIOProcID, refCon->mOutputDevice, state); if(state == IOState::Stopped || state == IOState::Stopping) { // Return early, since we just asked to stop. (Or something really weird is going on.) FillWithSilence(outOutputData); return noErr; } VCAssert(state == IOState::Running, "VCPlayThrough::OutputDeviceIOProc: Unexpected state"); if(didChangeState) { // We just changed state from Starting to Running, which means this is the first time this IOProc // has been called since the output device finished starting up, so now we can wake any threads // waiting in WaitForOutputDeviceToStart. VCAssert(refCon->mLastOutputSampleTime == -1, "VCPlayThrough::OutputDeviceIOProc: mLastOutputSampleTime not reset"); refCon->ReleaseThreadsWaitingForOutputToStart(); } if(refCon->mLastInputSampleTime == -1) { // Return early, since we don't have any data to output yet. FillWithSilence(outOutputData); return noErr; } // If this is the first time this IOProc has been called since starting playthrough... if(refCon->mLastOutputSampleTime == -1) { // Calculate the number of frames between the read and write heads refCon->mInToOutSampleOffset = inOutputTime->mSampleTime - refCon->mLastInputSampleTime; // Log if we dropped frames refCon->mRTLogger.LogIfDroppedFrames(refCon->mFirstInputSampleTime, refCon->mLastInputSampleTime); } CARingBuffer::SampleTime readHeadSampleTime = static_cast(inOutputTime->mSampleTime - refCon->mInToOutSampleOffset); CARingBuffer::SampleTime lastInputSampleTime = static_cast(refCon->mLastInputSampleTime); UInt32 framesToOutput = outOutputData->mBuffers[0].mDataByteSize / (SizeOf32(Float32) * 2); // When the input and output devices are set, during start up or because the user changed the // output device, this class (re)allocates the ring buffer (mBuffer). We try to take this // lock before accessing the buffer to make sure it's allocated. // // If we don't get the lock, another thread must be allocating or deallocating it, so we just // give up. We can't avoid audio glitches while changing devices anyway. This class tries to // make sure the IOProcs aren't running when it allocates the buffer, but it can't guarantee // that. // // Note that this is only realtime safe because we only try to lock the mutex. If another // thread has the mutex, it will be a non-realtime thread, so we can't wait for it. CAMutex::Tryer tryer(refCon->mBufferOutputMutex); // Disable a warning about accessing mBuffer without holding both mBufferInputMutex and // mBufferOutputMutex. The input IOProc always writes ahead of where the output IOProc will read // in a given IO cycle, so it's safe for them to read and write at the same time. #pragma clang diagnostic push #pragma clang diagnostic ignored "-Wthread-safety" if(tryer.HasLock() && refCon->mBuffer) { // Very occasionally (at least for me) our read head gets ahead of input, i.e. we haven't // received any new input since this IOProc was last called, and we have to recalculate its // position. I figure this might be caused by clock drift but I'm really not sure. It also // happens if the input or output sample times are restarted from zero. // // We also recalculate the offset if the read head is outside of the ring buffer. This // happens for example when you plug in or unplug headphones, which causes the output sample // times to be restarted from zero. // // The vast majority of the time, just using lastInputSampleTime as the read head time // instead of the one we calculate would work fine (and would also account for the above). SInt64 bufferStartTime, bufferEndTime; CARingBufferError err = refCon->mBuffer->GetTimeBounds(bufferStartTime, bufferEndTime); bool outOfBounds = false; if(err == kCARingBufferError_OK) { outOfBounds = (readHeadSampleTime < bufferStartTime) || (readHeadSampleTime - framesToOutput > bufferEndTime); } if(lastInputSampleTime < readHeadSampleTime || outOfBounds) { refCon->mRTLogger.LogNoSamplesReady(lastInputSampleTime, readHeadSampleTime, refCon->mInToOutSampleOffset); // Recalculate the in-to-out offset and read head. refCon->mInToOutSampleOffset = inOutputTime->mSampleTime - lastInputSampleTime; readHeadSampleTime = static_cast( inOutputTime->mSampleTime - refCon->mInToOutSampleOffset); } // Copy the frames from the ring buffer. err = refCon->mBuffer->Fetch(outOutputData, framesToOutput, readHeadSampleTime); refCon->mRTLogger.LogIfRingBufferError_Fetch(err); if(err != kCARingBufferError_OK) { FillWithSilence(outOutputData); } } else { refCon->mRTLogger.LogRingBufferUnavailable("OutputDeviceIOProc", tryer.HasLock()); FillWithSilence(outOutputData); } #pragma clang diagnostic pop refCon->mLastOutputSampleTime = inOutputTime->mSampleTime; return noErr; } // static inline void VCPlayThrough::FillWithSilence(AudioBufferList* ioBuffer) { for(UInt32 i = 0; i < ioBuffer->mNumberBuffers; i++) { memset(ioBuffer->mBuffers[i].mData, 0, ioBuffer->mBuffers[i].mDataByteSize); } } // static bool VCPlayThrough::UpdateIOProcState(const char* inCallerName, VCPlayThroughRTLogger& inRTLogger, std::atomic& inState, AudioDeviceIOProcID __nullable inIOProcID, VCAudioDevice& inDevice, IOState& outNewState) { VCAssert(inIOProcID != nullptr, "VCPlayThrough::UpdateIOProcState: !inIOProcID"); // Change this IOProc's state to Running if this is the first time it's been called since we // started playthrough. // // compare_exchange_strong will return true iff it changed inState from Starting to Running. // Otherwise it will set prevState to the current value of inState. // // TODO: We probably don't actually need memory_order_seq_cst (the default). Would it be worth // changing? Might be worth checking for the other atomics/barriers in this class, too. IOState prevState = IOState::Starting; bool didChangeState = inState.compare_exchange_strong(prevState, IOState::Running); if(didChangeState) { VCAssert(prevState == IOState::Starting, "VCPlayThrough::UpdateIOProcState: ?!"); outNewState = IOState::Running; } else { // Return the current value of inState to the caller. outNewState = prevState; if(outNewState != IOState::Running) { // The IOProc isn't Starting or Running, so it must be Stopping. That is, it's been // told to stop itself. VCAssert(outNewState == IOState::Stopping, "VCPlayThrough::UpdateIOProcState: Unexpected state: %d", outNewState); bool stoppedSuccessfully = false; try { inDevice.StopIOProc(inIOProcID); // StopIOProc didn't throw, so the IOProc won't be called again until the next // time playthrough is started. stoppedSuccessfully = true; } catch(CAException e) { inRTLogger.LogExceptionStoppingIOProc(inCallerName, e.GetError()); } catch(...) { inRTLogger.LogExceptionStoppingIOProc(inCallerName); } if(stoppedSuccessfully) { // Change inState to Stopped. // // If inState has been changed since we last read it, we don't know if we called // StopIOProc before or after the thread that changed it called StartIOProc (if it // did). However, inState is only changed here (in the IOProc), in Start and in // Stop. // // Stop won't return until the IOProc has changed inState to Stopped, unless it // times out, so Stop should still be waiting. And since Start and Stop are // mutually exclusive, this should be safe. // // But if Stop has timed out and inState has changed, we leave it in its new // state (unless there's some ABA problem thing happening), which I suspect is // the safest option. didChangeState = inState.compare_exchange_strong(outNewState, IOState::Stopped); if(didChangeState) { outNewState = IOState::Stopped; } else { inRTLogger.LogUnexpectedIOStateAfterStopping(inCallerName, static_cast(outNewState)); } } } } return didChangeState; }