mirror of
https://github.com/morgan9e/VolumeControl
synced 2026-04-14 08:14:06 +09:00
1251 lines
51 KiB
C++
1251 lines
51 KiB
C++
// 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 <http://www.gnu.org/licenses/>.
|
|
|
|
//
|
|
// 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 <algorithm> // For std::max
|
|
|
|
// System Includes
|
|
#include <mach/mach_init.h>
|
|
#include <mach/mach_time.h>
|
|
#include <mach/task.h>
|
|
|
|
|
|
// 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<CARingBuffer>(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<Float64>(startedBy - mToldOutputDeviceToStartAt) * base / NSEC_PER_MSEC,
|
|
static_cast<Float64>(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<UInt64>(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<UInt64>(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<VCPlayThrough*>(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<CFBooleanRef>(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<VCPlayThrough*>(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<CARingBuffer::SampleTime>(
|
|
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<VCPlayThrough*>(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<CARingBuffer::SampleTime>(inOutputTime->mSampleTime - refCon->mInToOutSampleOffset);
|
|
CARingBuffer::SampleTime lastInputSampleTime =
|
|
static_cast<CARingBuffer::SampleTime>(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<CARingBuffer::SampleTime>(
|
|
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<IOState>& 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<int>(outNewState));
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
return didChangeState;
|
|
}
|
|
|