// 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 */