// 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.h
// VCApp
//
// Copyright © 2016, 2017, 2020 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 VCDevice and the output device set to the one selected in the preferences menu.
//
// Apple's CAPlayThrough sample code (https://developer.apple.com/library/mac/samplecode/CAPlayThrough/Introduction/Intro.html)
// has a similar class, but I couldn't get it fast enough to use here. Soundflower also has a similar class
// (https://github.com/mattingalls/Soundflower/blob/master/SoundflowerBed/AudioThruEngine.h) that seems to be based on Apple
// sample code from 2004. This class's main addition is pausing playthrough when idle to save CPU.
//
// Playing audio with this class uses more CPU, mostly in the coreaudiod process, than playing audio normally because we need
// an input IOProc as well as an output one, and VCDriver is running in addition to the output device's driver. For me, it
// usually adds around 1-2% (as a percentage of total usage -- it doesn't seem to be relative to the CPU used when playing
// audio normally).
//
// This class will hopefully not be needed after CoreAudio's aggregate devices get support for controls, which is planned for
// a future release.
//
#ifndef VCApp__VCPlayThrough
#define VCApp__VCPlayThrough
// Local Includes
#include "VCAudioDevice.h"
#include "VCPlayThroughRTLogger.h"
// PublicUtility Includes
#include "CAMutex.h"
#include "CARingBuffer.h"
#include "VCThreadSafetyAnalysis.h"
// STL Includes
#include
#include
#include
// System Includes
#include
#pragma clang assume_nonnull begin
class VCPlayThrough
{
public:
// Error codes
static const OSStatus kDeviceNotStarting = 100;
public:
VCPlayThrough(VCAudioDevice inInputDevice, VCAudioDevice inOutputDevice);
~VCPlayThrough();
// Disallow copying
VCPlayThrough(const VCPlayThrough&) = delete;
VCPlayThrough& operator=(const VCPlayThrough&) = delete;
#ifdef __OBJC__
// Only intended as a convenience (hack) for Objective-C instance vars. Call
// SetDevices to initialise the instance before using it.
VCPlayThrough() { }
#endif
private:
/*! @throws CAException */
void Init(VCAudioDevice inInputDevice, VCAudioDevice inOutputDevice)
REQUIRES(mStateMutex);
public:
/*! @throws CAException */
void Activate();
/*! @throws CAException */
void Deactivate();
private:
void AllocateBuffer() REQUIRES(mStateMutex);
void DeallocateBuffer();
/*! @throws CAException */
void CreateIOProcIDs();
/*! @throws CAException */
void DestroyIOProcIDs();
/*!
@return True if both IOProcs are stopped.
@nonthreadsafe
*/
bool CheckIOProcsAreStopped() const noexcept REQUIRES(mStateMutex);
public:
/*!
Pass null for either param to only change one of the devices.
@throws CAException
*/
void SetDevices(const VCAudioDevice* __nullable inInputDevice,
const VCAudioDevice* __nullable inOutputDevice);
/*! @throws CAException */
void Start();
// Blocks until the output device has started our IOProc. Returns one of the error constants
// from AudioHardwareBase.h (e.g. kAudioHardwareNoError).
OSStatus WaitForOutputDeviceToStart() noexcept;
private:
/*! Real-time safe. */
void ReleaseThreadsWaitingForOutputToStart();
public:
OSStatus Stop();
void StopIfIdle();
private:
static OSStatus VCDeviceListenerProc(AudioObjectID inObjectID,
UInt32 inNumberAddresses,
const AudioObjectPropertyAddress* inAddresses,
void* __nullable inClientData);
static void HandleVCDeviceIsRunning(VCPlayThrough* refCon);
static void HandleVCDeviceIsRunningSomewhereOtherThanVCApp(VCPlayThrough* refCon);
static bool IsRunningSomewhereOtherThanVCApp(const VCAudioDevice& inVCDevice);
static OSStatus InputDeviceIOProc(AudioObjectID inDevice,
const AudioTimeStamp* inNow,
const AudioBufferList* inInputData,
const AudioTimeStamp* inInputTime,
AudioBufferList* outOutputData,
const AudioTimeStamp* inOutputTime,
void* __nullable inClientData);
static OSStatus OutputDeviceIOProc(AudioObjectID inDevice,
const AudioTimeStamp* inNow,
const AudioBufferList* inInputData,
const AudioTimeStamp* inInputTime,
AudioBufferList* outOutputData,
const AudioTimeStamp* inOutputTime,
void* __nullable inClientData);
/*! Fills the given ABL with zeroes to make it silent. */
static inline void FillWithSilence(AudioBufferList* ioBuffer);
// The state of an IOProc. Used by the IOProc to tell other threads when it's finished starting. Used by other
// threads to tell the IOProc to stop itself. (Probably used for other things as well.)
enum class IOState
{
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* inCallerName,
VCPlayThroughRTLogger& inRTLogger,
std::atomic& inState,
AudioDeviceIOProcID __nullable inIOProcID,
VCAudioDevice& inDevice,
IOState& outNewState);
private:
std::unique_ptr mBuffer PT_GUARDED_BY(mBufferInputMutex)
PT_GUARDED_BY(mBufferOutputMutex) { nullptr };
AudioDeviceIOProcID __nullable mInputDeviceIOProcID { nullptr };
AudioDeviceIOProcID __nullable mOutputDeviceIOProcID { nullptr };
VCAudioDevice mInputDevice { kAudioObjectUnknown };
VCAudioDevice mOutputDevice { kAudioObjectUnknown };
// mStateMutex is the general purpose mutex. mBufferInputMutex and mBufferOutputMutex are
// just used to make sure mBuffer, the ring buffer, is allocated when the IOProcs access it. See
// the comments in the IOProcs for details.
//
// If a thread might lock more than one of these mutexes, it *must* take them in this order:
// 1. mStateMutex
// 2. mBufferInputMutex
// 3. mBufferOutputMutex
//
// The ACQUIRED_BEFORE annotations don't do anything yet. From clang's docs: "ACQUIRED_BEFORE(…)
// and ACQUIRED_AFTER(…) are currently unimplemented. To be fixed in a future update." After
// they've fixed that, the compiler will enforce the ordering statically.
//
// TODO: We can't use std::shared_lock because we're still on C++11, but we could use std::lock
// to help ensure the locks are always taken in the right order.
// TODO: It would be better to have a separate class for the buffer and its mutexes.
CAMutex mStateMutex ACQUIRED_BEFORE(mBufferInputMutex)
ACQUIRED_BEFORE(mBufferOutputMutex) { "Playthrough state" };
CAMutex mBufferInputMutex ACQUIRED_BEFORE(mBufferOutputMutex)
{ "Playthrough ring buffer input" };
CAMutex mBufferOutputMutex { "Playthrough ring buffer output" };
// Signalled when the output IOProc runs. We use it to tell VCDriver when the output device is ready to receive audio data.
semaphore_t mOutputDeviceIOProcSemaphore { SEMAPHORE_NULL };
bool mActive = false;
bool mPlayingThrough = false;
UInt64 mLastNotifiedIOStoppedOnVCDevice { 0 };
std::atomic mInputDeviceIOProcState { IOState::Stopped };
std::atomic mOutputDeviceIOProcState { IOState::Stopped };
// For debug logging.
UInt64 mToldOutputDeviceToStartAt { 0 };
// IOProc vars. (Should only be used inside IOProcs.)
// The earliest/latest sample times seen by the IOProcs since starting playthrough. -1 for unset.
Float64 mFirstInputSampleTime = -1;
Float64 mLastInputSampleTime = -1;
Float64 mLastOutputSampleTime = -1;
// Subtract this from the output time to get the input time.
Float64 mInToOutSampleOffset { 0.0 };
VCPlayThroughRTLogger mRTLogger;
};
#pragma clang assume_nonnull end
#endif /* VCApp__VCPlayThrough */