diff --git a/README.md b/README.md index dcd7de02..ea21dca6 100644 --- a/README.md +++ b/README.md @@ -4,6 +4,7 @@ YUP is a C++20 framework for building native applications, audio tools, and audio plugins with one codebase across desktop, mobile, and the web. It combines permissively licensed JUCE7-derived foundations with modern rendering through the open source [Rive](https://rive.app/) renderer and YUP's own evolving graphics, GUI, DSP, audio graph, and plugin layers. +[![Ask DeepWiki](https://deepwiki.com/badge.svg)](https://deepwiki.com/kunitoki/yup) [![Build And Test MacOS](https://github.com/kunitoki/yup/actions/workflows/build_macos.yml/badge.svg)](https://github.com/kunitoki/yup/actions/workflows/build_macos.yml) [![Build And Test Windows](https://github.com/kunitoki/yup/actions/workflows/build_windows.yml/badge.svg)](https://github.com/kunitoki/yup/actions/workflows/build_windows.yml) [![Build And Test Linux](https://github.com/kunitoki/yup/actions/workflows/build_linux.yml/badge.svg)](https://github.com/kunitoki/yup/actions/workflows/build_linux.yml) diff --git a/examples/audiograph/source/AudioGraphApp.cpp b/examples/audiograph/source/AudioGraphApp.cpp index a3ab4d9f..894c46aa 100644 --- a/examples/audiograph/source/AudioGraphApp.cpp +++ b/examples/audiograph/source/AudioGraphApp.cpp @@ -127,6 +127,7 @@ void AudioGraphApp::resized() saveButton.setBounds (toolbar.removeFromLeft (70).reduced (4, 4)); toolbar.removeFromLeft (8); scanButton.setBounds (toolbar.removeFromLeft (100).reduced (4, 4)); + settingsButton.setBounds (toolbar.removeFromLeft (80).reduced (4, 4)); statusLabel.setBounds (toolbar.reduced (4, 4)); editorPanel->setBounds (bounds); @@ -245,6 +246,16 @@ void AudioGraphApp::setupToolbar() }; addAndMakeVisible (scanButton); + settingsButton.setButtonText ("Settings"); + settingsButton.onClick = [this] + { + if (deviceManagerWindow == nullptr) + deviceManagerWindow = std::make_unique (deviceManager); + deviceManagerWindow->setVisible (true); + deviceManagerWindow->toFront (true); + }; + addAndMakeVisible (settingsButton); + statusLabel.setText ("Ready.", yup::dontSendNotification); addAndMakeVisible (statusLabel); } diff --git a/examples/audiograph/source/AudioGraphApp.h b/examples/audiograph/source/AudioGraphApp.h index e96f194b..35703ec9 100644 --- a/examples/audiograph/source/AudioGraphApp.h +++ b/examples/audiograph/source/AudioGraphApp.h @@ -147,8 +147,11 @@ class AudioGraphApp final yup::TextButton openButton; yup::TextButton saveButton; yup::TextButton scanButton; + yup::TextButton settingsButton; yup::Label statusLabel; + std::unique_ptr deviceManagerWindow; + bool audioCallbackRegistered = false; #if YUP_DESKTOP diff --git a/modules/yup_audio_devices/native/yup_CoreAudio_mac.cpp b/modules/yup_audio_devices/native/yup_CoreAudio_mac.cpp index 5f0ea733..6deb407b 100644 --- a/modules/yup_audio_devices/native/yup_CoreAudio_mac.cpp +++ b/modules/yup_audio_devices/native/yup_CoreAudio_mac.cpp @@ -1,2395 +1,2692 @@ -/* - ============================================================================== - - This file is part of the YUP library. - Copyright (c) 2024 - kunitoki@gmail.com - - YUP is an open source library subject to open-source licensing. - - The code included in this file is provided under the terms of the ISC license - http://www.isc.org/downloads/software-support-policy/isc-license. Permission - to use, copy, modify, and/or distribute this software for any purpose with or - without fee is hereby granted provided that the above copyright notice and - this permission notice appear in all copies. - - YUP IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER - EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE - DISCLAIMED. - - ============================================================================== - - This file is part of the JUCE library. - Copyright (c) 2022 - Raw Material Software Limited - - JUCE is an open source library subject to commercial or open-source - licensing. - - The code included in this file is provided under the terms of the ISC license - http://www.isc.org/downloads/software-support-policy/isc-license. Permission - To use, copy, modify, and/or distribute this software for any purpose with or - without fee is hereby granted provided that the above copyright notice and - this permission notice appear in all copies. - - JUCE IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER - EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE - DISCLAIMED. - - ============================================================================== -*/ - -namespace yup -{ - -#if YUP_COREAUDIO_LOGGING_ENABLED -#define YUP_COREAUDIOLOG(a) \ - { \ - String camsg ("CoreAudio: "); \ - camsg << a; \ - Logger::writeToLog (camsg); \ - } -#else -#define YUP_COREAUDIOLOG(a) -#endif - -YUP_BEGIN_IGNORE_WARNINGS_GCC_LIKE ("-Wnonnull") - -constexpr auto yupAudioObjectPropertyElementMain = -#if defined(MAC_OS_VERSION_12_0) - kAudioObjectPropertyElementMain; -#else - kAudioObjectPropertyElementMaster; -#endif - -//============================================================================== -class ManagedAudioBufferList final : public AudioBufferList -{ -public: - struct Deleter - { - void operator() (ManagedAudioBufferList* p) const - { - if (p != nullptr) - p->~ManagedAudioBufferList(); - - delete[] reinterpret_cast (p); - } - }; - - using Ref = std::unique_ptr; - - //============================================================================== - static Ref create (std::size_t numBuffers) - { - static_assert (alignof (ManagedAudioBufferList) <= alignof (std::max_align_t)); - - if (std::unique_ptr storage { new std::byte[storageSizeForNumBuffers (numBuffers)] }) - return Ref { new (storage.release()) ManagedAudioBufferList (numBuffers) }; - - return nullptr; - } - - //============================================================================== - static std::size_t storageSizeForNumBuffers (std::size_t numBuffers) noexcept - { - return audioBufferListHeaderSize + (numBuffers * sizeof (::AudioBuffer)); - } - - static std::size_t numBuffersForStorageSize (std::size_t bytes) noexcept - { - bytes -= audioBufferListHeaderSize; - - // storage size ends between to buffers in AudioBufferList - jassert ((bytes % sizeof (::AudioBuffer)) == 0); - - return bytes / sizeof (::AudioBuffer); - } - -private: - // Do not call the base constructor here as this will zero-initialize the first buffer, - // for which no storage may be available though (when numBuffers == 0). - explicit ManagedAudioBufferList (std::size_t numBuffers) - { - mNumberBuffers = static_cast (numBuffers); - } - - static constexpr auto audioBufferListHeaderSize = sizeof (AudioBufferList) - sizeof (::AudioBuffer); - - YUP_DECLARE_NON_COPYABLE (ManagedAudioBufferList) - YUP_DECLARE_NON_MOVEABLE (ManagedAudioBufferList) -}; - -//============================================================================== -struct IgnoreUnused -{ - template - void operator() (Ts&&...) const - { - } -}; - -template -static auto getDataPtrAndSize (T& t) -{ - static_assert (std::is_pod_v); - return std::make_tuple (&t, (UInt32) sizeof (T)); -} - -static auto getDataPtrAndSize (ManagedAudioBufferList::Ref& t) -{ - const auto size = t.get() != nullptr - ? ManagedAudioBufferList::storageSizeForNumBuffers (t->mNumberBuffers) - : 0; - return std::make_tuple (t.get(), (UInt32) size); -} - -//============================================================================== -[[nodiscard]] static bool audioObjectHasProperty (AudioObjectID objectID, const AudioObjectPropertyAddress address) -{ - return objectID != kAudioObjectUnknown && AudioObjectHasProperty (objectID, &address); -} - -template -[[nodiscard]] static auto audioObjectGetProperty (AudioObjectID objectID, - const AudioObjectPropertyAddress address, - OnError&& onError = {}) -{ - using Result = std::conditional_t, ManagedAudioBufferList::Ref, std::optional>; - - if (! audioObjectHasProperty (objectID, address)) - return Result {}; - - auto result = [&] - { - if constexpr (std::is_same_v) - { - UInt32 size {}; - - if (auto status = AudioObjectGetPropertyDataSize (objectID, &address, 0, nullptr, &size); status != noErr) - { - onError (status); - return Result {}; - } - - return ManagedAudioBufferList::create (ManagedAudioBufferList::numBuffersForStorageSize (size)); - } - else - { - return T {}; - } - }(); - - auto [ptr, size] = getDataPtrAndSize (result); - - if (size == 0) - return Result {}; - - if (auto status = AudioObjectGetPropertyData (objectID, &address, 0, nullptr, &size, ptr); status != noErr) - { - onError (status); - return Result {}; - } - - return Result { std::move (result) }; -} - -template -static bool audioObjectSetProperty (AudioObjectID objectID, - const AudioObjectPropertyAddress address, - const T value, - OnError&& onError = {}) -{ - if (! audioObjectHasProperty (objectID, address)) - return false; - - Boolean isSettable = NO; - if (auto status = AudioObjectIsPropertySettable (objectID, &address, &isSettable); status != noErr) - { - onError (status); - return false; - } - - if (! isSettable) - return false; - - if (auto status = AudioObjectSetPropertyData (objectID, &address, 0, nullptr, static_cast (sizeof (T)), &value); status != noErr) - { - onError (status); - return false; - } - - return true; -} - -template -[[nodiscard]] static std::vector audioObjectGetProperties (AudioObjectID objectID, - const AudioObjectPropertyAddress address, - OnError&& onError = {}) -{ - if (! audioObjectHasProperty (objectID, address)) - return {}; - - UInt32 size {}; - - if (auto status = AudioObjectGetPropertyDataSize (objectID, &address, 0, nullptr, &size); status != noErr) - { - onError (status); - return {}; - } - - // If this is hit, the number of results is not integral, and the following - // AudioObjectGetPropertyData will probably write past the end of the result buffer. - jassert ((size % sizeof (T)) == 0); - std::vector result (size / sizeof (T)); - - if (auto status = AudioObjectGetPropertyData (objectID, &address, 0, nullptr, &size, result.data()); status != noErr) - { - onError (status); - return {}; - } - - return result; -} - -//============================================================================== -struct AsyncRestarter -{ - virtual ~AsyncRestarter() = default; - virtual void restartAsync() = 0; -}; - -struct SystemVol -{ - explicit SystemVol (AudioObjectPropertySelector selector) noexcept - : outputDeviceID (audioObjectGetProperty (kAudioObjectSystemObject, { kAudioHardwarePropertyDefaultOutputDevice, kAudioObjectPropertyScopeGlobal, yupAudioObjectPropertyElementMain }).value_or (kAudioObjectUnknown)) - , addr { selector, kAudioDevicePropertyScopeOutput, yupAudioObjectPropertyElementMain } - { - } - - float getGain() const noexcept - { - return audioObjectGetProperty (outputDeviceID, addr).value_or (0.0f); - } - - bool setGain (float gain) const noexcept - { - return audioObjectSetProperty (outputDeviceID, addr, static_cast (gain)); - } - - bool isMuted() const noexcept - { - return audioObjectGetProperty (outputDeviceID, addr).value_or (0) != 0; - } - - bool setMuted (bool mute) const noexcept - { - return audioObjectSetProperty (outputDeviceID, addr, static_cast (mute ? 1 : 0)); - } - -private: - AudioDeviceID outputDeviceID; - AudioObjectPropertyAddress addr; -}; - -YUP_END_IGNORE_WARNINGS_GCC_LIKE - -constexpr auto yupAudioHardwareServiceDeviceProperty_VirtualMainVolume = -#if defined(MAC_OS_VERSION_12_0) - kAudioHardwareServiceDeviceProperty_VirtualMainVolume; -#else - kAudioHardwareServiceDeviceProperty_VirtualMasterVolume; -#endif - -#define YUP_SYSTEMAUDIOVOL_IMPLEMENTED 1 - -float YUP_CALLTYPE SystemAudioVolume::getGain() -{ - return SystemVol (yupAudioHardwareServiceDeviceProperty_VirtualMainVolume).getGain(); -} - -bool YUP_CALLTYPE SystemAudioVolume::setGain (float gain) { return SystemVol (yupAudioHardwareServiceDeviceProperty_VirtualMainVolume).setGain (gain); } - -bool YUP_CALLTYPE SystemAudioVolume::isMuted() { return SystemVol (kAudioDevicePropertyMute).isMuted(); } - -bool YUP_CALLTYPE SystemAudioVolume::setMuted (bool mute) { return SystemVol (kAudioDevicePropertyMute).setMuted (mute); } - -//============================================================================== -struct CoreAudioClasses -{ - class CoreAudioIODeviceType; - class CoreAudioIODevice; - - //============================================================================== - class CoreAudioInternal final : private Timer - , private AsyncUpdater - { - private: - // members with deduced return types need to be defined before they - // are used, so define it here. decltype doesn't help as you can't - // capture anything in lambdas inside a decltype context. - auto err2log() const - { - return [this] (OSStatus err) - { - OK (err); - }; - } - - public: - CoreAudioInternal (CoreAudioIODevice& d, AudioDeviceID id, bool hasInput, bool hasOutput) - : owner (d) - , deviceID (id) - , inStream (hasInput ? new Stream (true, *this, {}) : nullptr) - , outStream (hasOutput ? new Stream (false, *this, {}) : nullptr) - { - jassert (deviceID != 0); - - updateDetailsFromDevice(); - YUP_COREAUDIOLOG ("Creating CoreAudioInternal\n" - << (inStream != nullptr ? (" inputDeviceId " + String (deviceID) + "\n") : "") - << (outStream != nullptr ? (" outputDeviceId " + String (deviceID) + "\n") : "") - << getDeviceDetails().joinIntoString ("\n ")); - - AudioObjectPropertyAddress pa; - pa.mSelector = kAudioObjectPropertySelectorWildcard; - pa.mScope = kAudioObjectPropertyScopeWildcard; - pa.mElement = kAudioObjectPropertyElementWildcard; - - AudioObjectAddPropertyListener (deviceID, &pa, deviceListenerProc, this); - } - - ~CoreAudioInternal() override - { - stopTimer(); - cancelPendingUpdate(); - - AudioObjectPropertyAddress pa; - pa.mSelector = kAudioObjectPropertySelectorWildcard; - pa.mScope = kAudioObjectPropertyScopeWildcard; - pa.mElement = kAudioObjectPropertyElementWildcard; - - AudioObjectRemovePropertyListener (deviceID, &pa, deviceListenerProc, this); - - stop (false); - } - - auto getStreams() const { return std::array { { inStream.get(), outStream.get() } }; } - - void allocateTempBuffers() - { - auto tempBufSize = bufferSize + 4; - - auto streams = getStreams(); - const auto total = std::accumulate (streams.begin(), streams.end(), 0, [] (int n, const auto& s) - { - return n + (s != nullptr ? s->channels : 0); - }); - audioBuffer.calloc (total * tempBufSize); - - auto channels = 0; - for (auto* stream : streams) - channels += stream != nullptr ? stream->allocateTempBuffers (tempBufSize, channels, audioBuffer) : 0; - } - - struct CallbackDetailsForChannel - { - int streamNum; - int dataOffsetSamples; - int dataStrideSamples; - }; - - Array getSampleRatesFromDevice() const - { - Array newSampleRates; - - if (auto ranges = audioObjectGetProperties (deviceID, - { kAudioDevicePropertyAvailableNominalSampleRates, - kAudioObjectPropertyScopeWildcard, - yupAudioObjectPropertyElementMain }, - err2log()); - ! ranges.empty()) - { - for (const auto rate : SampleRateHelpers::getAllSampleRates()) - { - for (auto range = ranges.rbegin(); range != ranges.rend(); ++range) - { - if (range->mMinimum - 2 <= rate && rate <= range->mMaximum + 2) - { - newSampleRates.add (rate); - break; - } - } - } - } - - if (newSampleRates.isEmpty() && sampleRate > 0) - newSampleRates.add (sampleRate); - - auto nominalRate = getNominalSampleRate(); - - if ((nominalRate > 0) && ! newSampleRates.contains (nominalRate)) - newSampleRates.addUsingDefaultSort (nominalRate); - - return newSampleRates; - } - - Array getBufferSizesFromDevice() const - { - Array newBufferSizes; - - if (auto ranges = audioObjectGetProperties (deviceID, { kAudioDevicePropertyBufferFrameSizeRange, kAudioObjectPropertyScopeWildcard, yupAudioObjectPropertyElementMain }, err2log()); ! ranges.empty()) - { - newBufferSizes.add ((int) (ranges[0].mMinimum + 15) & ~15); - - for (int i = 32; i <= 2048; i += 32) - { - for (auto range = ranges.rbegin(); range != ranges.rend(); ++range) - { - if (i >= range->mMinimum && i <= range->mMaximum) - { - newBufferSizes.addIfNotAlreadyThere (i); - break; - } - } - } - - if (bufferSize > 0) - newBufferSizes.addIfNotAlreadyThere (bufferSize); - } - - if (newBufferSizes.isEmpty() && bufferSize > 0) - newBufferSizes.add (bufferSize); - - return newBufferSizes; - } - - int getFrameSizeFromDevice() const - { - return static_cast (audioObjectGetProperty (deviceID, { kAudioDevicePropertyBufferFrameSize, kAudioObjectPropertyScopeWildcard, yupAudioObjectPropertyElementMain }).value_or (0)); - } - - bool isDeviceAlive() const - { - return deviceID != 0 - && audioObjectGetProperty (deviceID, { kAudioDevicePropertyDeviceIsAlive, kAudioObjectPropertyScopeWildcard, yupAudioObjectPropertyElementMain }, err2log()).value_or (0) != 0; - } - - bool updateDetailsFromDevice (const BigInteger& activeIns, const BigInteger& activeOuts) - { - stopTimer(); - - if (! isDeviceAlive()) - return false; - - // this collects all the new details from the device without any locking, then - // locks + swaps them afterwards. - - auto newSampleRate = getNominalSampleRate(); - auto newBufferSize = getFrameSizeFromDevice(); - - auto newBufferSizes = getBufferSizesFromDevice(); - auto newSampleRates = getSampleRatesFromDevice(); - - auto newInput = rawToUniquePtr (inStream != nullptr ? new Stream (true, *this, activeIns) : nullptr); - auto newOutput = rawToUniquePtr (outStream != nullptr ? new Stream (false, *this, activeOuts) : nullptr); - - auto newBitDepth = jmax (getBitDepth (newInput), getBitDepth (newOutput)); - -#if YUP_AUDIOWORKGROUP_TYPES_AVAILABLE - audioWorkgroup = [this]() -> AudioWorkgroup - { - AudioObjectPropertyAddress pa; - pa.mSelector = kAudioDevicePropertyIOThreadOSWorkgroup; - pa.mScope = kAudioObjectPropertyScopeWildcard; - pa.mElement = yupAudioObjectPropertyElementMain; - - if (auto* workgroupHandle = audioObjectGetProperty (deviceID, pa).value_or (nullptr)) - { - os_workgroup_t workgroup = (__bridge_transfer os_workgroup_t) workgroupHandle; - return makeRealAudioWorkgroup (workgroup); - } - - return {}; - }(); -#endif - - { - const ScopedLock sl (callbackLock); - - bitDepth = newBitDepth > 0 ? newBitDepth : 32; - - if (newSampleRate > 0) - sampleRate = newSampleRate; - - bufferSize = newBufferSize; - - sampleRates.swapWith (newSampleRates); - bufferSizes.swapWith (newBufferSizes); - - std::swap (inStream, newInput); - std::swap (outStream, newOutput); - - allocateTempBuffers(); - } - - return true; - } - - bool updateDetailsFromDevice() - { - return updateDetailsFromDevice (getActiveChannels (inStream), getActiveChannels (outStream)); - } - - StringArray getDeviceDetails() - { - StringArray result; - - String availableSampleRates ("Available sample rates:"); - - for (auto& s : sampleRates) - availableSampleRates << " " << s; - - result.add (availableSampleRates); - result.add ("Sample rate: " + String (sampleRate)); - String availableBufferSizes ("Available buffer sizes:"); - - for (auto& b : bufferSizes) - availableBufferSizes << " " << b; - - result.add (availableBufferSizes); - result.add ("Buffer size: " + String (bufferSize)); - result.add ("Bit depth: " + String (bitDepth)); - result.add ("Input latency: " + String (getLatency (inStream))); - result.add ("Output latency: " + String (getLatency (outStream))); - result.add ("Input channel names: " + getChannelNames (inStream)); - result.add ("Output channel names: " + getChannelNames (outStream)); - - return result; - } - - static auto getScope (bool input) - { - return input ? kAudioDevicePropertyScopeInput : kAudioDevicePropertyScopeOutput; - } - - //============================================================================== - StringArray getSources (bool input) - { - StringArray s; - auto types = audioObjectGetProperties (deviceID, { kAudioDevicePropertyDataSources, kAudioObjectPropertyScopeWildcard, yupAudioObjectPropertyElementMain }); - - for (auto type : types) - { - AudioValueTranslation avt; - char buffer[256]; - - avt.mInputData = &type; - avt.mInputDataSize = sizeof (UInt32); - avt.mOutputData = buffer; - avt.mOutputDataSize = 256; - - UInt32 transSize = sizeof (avt); - - AudioObjectPropertyAddress pa; - pa.mSelector = kAudioDevicePropertyDataSourceNameForID; - pa.mScope = getScope (input); - pa.mElement = yupAudioObjectPropertyElementMain; - - if (OK (AudioObjectGetPropertyData (deviceID, &pa, 0, nullptr, &transSize, &avt))) - s.add (buffer); - } - - return s; - } - - int getCurrentSourceIndex (bool input) const - { - if (deviceID != 0) - { - if (auto currentSourceID = audioObjectGetProperty (deviceID, { kAudioDevicePropertyDataSource, getScope (input), yupAudioObjectPropertyElementMain }, err2log())) - { - auto types = audioObjectGetProperties (deviceID, { kAudioDevicePropertyDataSources, kAudioObjectPropertyScopeWildcard, yupAudioObjectPropertyElementMain }); - - if (auto it = std::find (types.begin(), types.end(), *currentSourceID); it != types.end()) - return static_cast (std::distance (types.begin(), it)); - } - } - - return -1; - } - - void setCurrentSourceIndex (int index, bool input) - { - if (deviceID != 0) - { - auto types = audioObjectGetProperties (deviceID, { kAudioDevicePropertyDataSources, kAudioObjectPropertyScopeWildcard, yupAudioObjectPropertyElementMain }); - - if (isPositiveAndBelow (index, static_cast (types.size()))) - { - audioObjectSetProperty (deviceID, { kAudioDevicePropertyDataSource, getScope (input), yupAudioObjectPropertyElementMain }, types[static_cast (index)], err2log()); - } - } - } - - double getNominalSampleRate() const - { - return static_cast (audioObjectGetProperty (deviceID, { kAudioDevicePropertyNominalSampleRate, kAudioObjectPropertyScopeGlobal, yupAudioObjectPropertyElementMain }, err2log()).value_or (0.0)); - } - - bool setNominalSampleRate (double newSampleRate) const - { - if (std::abs (getNominalSampleRate() - newSampleRate) < 1.0) - return true; - - return audioObjectSetProperty (deviceID, { kAudioDevicePropertyNominalSampleRate, kAudioObjectPropertyScopeGlobal, yupAudioObjectPropertyElementMain }, static_cast (newSampleRate), err2log()); - } - - //============================================================================== - String reopen (const BigInteger& ins, const BigInteger& outs, double newSampleRate, int bufferSizeSamples) - { - callbacksAllowed = false; - const ScopeGuard scope { [&] - { - callbacksAllowed = true; - } }; - - stopTimer(); - - stop (false); - - if (! setNominalSampleRate (newSampleRate)) - { - updateDetailsFromDevice (ins, outs); - return "Couldn't change sample rate"; - } - - if (! audioObjectSetProperty (deviceID, { kAudioDevicePropertyBufferFrameSize, kAudioObjectPropertyScopeGlobal, yupAudioObjectPropertyElementMain }, static_cast (bufferSizeSamples), err2log())) - { - updateDetailsFromDevice (ins, outs); - return "Couldn't change buffer size"; - } - - // Annoyingly, after changing the rate and buffer size, some devices fail to - // correctly report their new settings until some random time in the future, so - // after calling updateDetailsFromDevice, we need to manually bodge these values - // to make sure we're using the correct numbers.. - updateDetailsFromDevice (ins, outs); - sampleRate = newSampleRate; - bufferSize = bufferSizeSamples; - - if (sampleRates.size() == 0) - return "Device has no available sample-rates"; - - if (bufferSizes.size() == 0) - return "Device has no available buffer-sizes"; - - return {}; - } - - bool start (AudioIODeviceCallback* callbackToNotify) - { - const ScopedLock sl (callbackLock); - - if (callback == nullptr && callbackToNotify != nullptr) - { - callback = callbackToNotify; - callback->audioDeviceAboutToStart (&owner); - } - - for (auto* stream : getStreams()) - if (stream != nullptr) - stream->previousSampleTime = invalidSampleTime; - - owner.hadDiscontinuity = false; - - if (scopedProcID.get() == nullptr && deviceID != 0) - { - scopedProcID = [&self = *this, - &lock = callbackLock, - nextProcID = ScopedAudioDeviceIOProcID { *this, deviceID, audioIOProc }, - dID = deviceID]() mutable -> ScopedAudioDeviceIOProcID - { - // It *looks* like AudioDeviceStart may start the audio callback running, and then - // immediately lock an internal mutex. - // The same mutex is locked before calling the audioIOProc. - // If we get very unlucky, then we can end up with thread A taking the callbackLock - // and calling AudioDeviceStart, followed by thread B taking the CoreAudio lock - // and calling into audioIOProc, which waits on the callbackLock. When thread A - // continues it attempts to take the CoreAudio lock, and the program deadlocks. - - if (auto* procID = nextProcID.get()) - { - const ScopedUnlock su (lock); - - if (self.OK (AudioDeviceStart (dID, procID))) - return std::move (nextProcID); - } - - return {}; - }(); - } - - playing = scopedProcID.get() != nullptr && callback != nullptr; - - return scopedProcID.get() != nullptr; - } - - AudioIODeviceCallback* stop (bool leaveInterruptRunning) - { - const ScopedLock sl (callbackLock); - - auto result = std::exchange (callback, nullptr); - - if (scopedProcID.get() != nullptr && (deviceID != 0) && ! leaveInterruptRunning) - { - audioDeviceStopPending = true; - - // wait until AudioDeviceStop() has been called on the IO thread - for (int i = 40; --i >= 0;) - { - if (audioDeviceStopPending == false) - break; - - const ScopedUnlock ul (callbackLock); - Thread::sleep (50); - } - - scopedProcID = {}; - playing = false; - } - - return result; - } - - double getSampleRate() const { return sampleRate; } - - int getBufferSize() const { return bufferSize; } - - void audioCallback (const AudioTimeStamp* inputTimestamp, - const AudioTimeStamp* outputTimestamp, - const AudioBufferList* inInputData, - AudioBufferList* outOutputData) - { - const ScopedLock sl (callbackLock); - - if (audioDeviceStopPending) - { - if (OK (AudioDeviceStop (deviceID, scopedProcID.get()))) - audioDeviceStopPending = false; - - return; - } - - const auto numInputChans = getChannels (inStream); - const auto numOutputChans = getChannels (outStream); - - if (callback != nullptr) - { - for (int i = numInputChans; --i >= 0;) - { - auto& info = inStream->channelInfo.getReference (i); - auto dest = inStream->tempBuffers[i]; - auto src = ((const float*) inInputData->mBuffers[info.streamNum].mData) + info.dataOffsetSamples; - auto stride = info.dataStrideSamples; - - if (stride != 0) // if this is zero, info is invalid - { - for (int j = bufferSize; --j >= 0;) - { - *dest++ = *src; - src += stride; - } - } - } - - for (auto* stream : getStreams()) - if (stream != nullptr) - owner.hadDiscontinuity |= stream->checkTimestampsForDiscontinuity (stream == inStream.get() ? inputTimestamp - : outputTimestamp); - - const auto* timeStamp = numOutputChans > 0 ? outputTimestamp : inputTimestamp; - const auto nanos = timeStamp != nullptr ? timeConversions.hostTimeToNanos (timeStamp->mHostTime) : 0; - const AudioIODeviceCallbackContext context { - timeStamp != nullptr ? &nanos : nullptr, - }; - - callback->audioDeviceIOCallbackWithContext (getTempBuffers (inStream), numInputChans, getTempBuffers (outStream), numOutputChans, bufferSize, context); - - for (int i = numOutputChans; --i >= 0;) - { - auto& info = outStream->channelInfo.getReference (i); - auto src = outStream->tempBuffers[i]; - auto dest = ((float*) outOutputData->mBuffers[info.streamNum].mData) + info.dataOffsetSamples; - auto stride = info.dataStrideSamples; - - if (stride != 0) // if this is zero, info is invalid - { - for (int j = bufferSize; --j >= 0;) - { - *dest = *src++; - dest += stride; - } - } - } - } - else - { - for (UInt32 i = 0; i < outOutputData->mNumberBuffers; ++i) - zeromem (outOutputData->mBuffers[i].mData, - outOutputData->mBuffers[i].mDataByteSize); - } - - for (auto* stream : getStreams()) - if (stream != nullptr) - stream->previousSampleTime += static_cast (bufferSize); - } - - // called by callbacks (possibly off the main thread) - void deviceDetailsChanged() - { - if (callbacksAllowed.get() == 1) - startTimer (100); - } - - // called by callbacks (possibly off the main thread) - void deviceRequestedRestart() - { - owner.restart(); - triggerAsyncUpdate(); - } - - bool isPlaying() const { return playing.load(); } - - //============================================================================== - struct Stream - { - Stream (bool isInput, CoreAudioInternal& parent, const BigInteger& activeRequested) - : input (isInput) - , latency (getLatencyFromDevice (isInput, parent)) - , bitDepth (getBitDepthFromDevice (isInput, parent)) - , chanNames (getChannelNames (isInput, parent)) - , activeChans ([&activeRequested, clearFrom = chanNames.size()] - { - auto result = activeRequested; - result.setRange (clearFrom, result.getHighestBit() + 1 - clearFrom, false); - return result; - }()) - , channelInfo (getChannelInfos (isInput, parent, activeChans)) - , channels (static_cast (channelInfo.size())) - { - } - - int allocateTempBuffers (int tempBufSize, int channelCount, HeapBlock& buffer) - { - tempBuffers.calloc (channels + 2); - - for (int i = 0; i < channels; ++i) - tempBuffers[i] = buffer + channelCount++ * tempBufSize; - - return channels; - } - - template - static auto visitChannels (bool isInput, CoreAudioInternal& parent, Visitor&& visitor) - { - struct Args - { - int stream, channelIdx, chanNum, streamChannels; - }; - - using VisitorResultType = typename std::invoke_result_t::value_type; - Array result; - int chanNum = 0; - - if (auto bufList = audioObjectGetProperty (parent.deviceID, { kAudioDevicePropertyStreamConfiguration, getScope (isInput), yupAudioObjectPropertyElementMain }, parent.err2log())) - { - const int numStreams = static_cast (bufList->mNumberBuffers); - - for (int i = 0; i < numStreams; ++i) - { - auto& b = bufList->mBuffers[i]; - - for (unsigned int j = 0; j < b.mNumberChannels; ++j) - { - // Passing an anonymous struct ensures that callback can't confuse the argument order - if (auto opt = visitor (Args { i, static_cast (j), chanNum++, static_cast (b.mNumberChannels) })) - result.add (std::move (*opt)); - } - } - } - - return result; - } - - static Array getChannelInfos (bool isInput, CoreAudioInternal& parent, const BigInteger& active) - { - return visitChannels (isInput, parent, [&] (const auto& args) -> std::optional - { - if (! active[args.chanNum]) - return {}; - - return CallbackDetailsForChannel { args.stream, args.channelIdx, args.streamChannels }; - }); - } - - static StringArray getChannelNames (bool isInput, CoreAudioInternal& parent) - { - auto names = visitChannels (isInput, parent, [&] (const auto& args) -> std::optional - { - String name; - const auto element = static_cast (args.chanNum + 1); - - if (auto retainedName = audioObjectGetProperty (parent.deviceID, { kAudioObjectPropertyElementName, getScope (isInput), element }).value_or (nullptr)) - { - const CFUniquePtr nameString { retainedName }; - name = String::fromCFString (nameString.get()); - } - - if (name.isEmpty()) - name << (isInput ? "Input " : "Output ") << (args.chanNum + 1); - - return name; - }); - - return { names }; - } - - static int getBitDepthFromDevice (bool isInput, CoreAudioInternal& parent) - { - return static_cast (audioObjectGetProperty (parent.deviceID, { kAudioStreamPropertyPhysicalFormat, getScope (isInput), yupAudioObjectPropertyElementMain }, parent.err2log()) - .value_or (AudioStreamBasicDescription {}) - .mBitsPerChannel); - } - - static int getLatencyFromDevice (bool isInput, CoreAudioInternal& parent) - { - const auto scope = getScope (isInput); - - const auto deviceLatency = audioObjectGetProperty (parent.deviceID, { kAudioDevicePropertyLatency, scope, yupAudioObjectPropertyElementMain }).value_or (0); - - const auto safetyOffset = audioObjectGetProperty (parent.deviceID, { kAudioDevicePropertySafetyOffset, scope, yupAudioObjectPropertyElementMain }).value_or (0); - - const auto framesInBuffer = audioObjectGetProperty (parent.deviceID, { kAudioDevicePropertyBufferFrameSize, kAudioObjectPropertyScopeWildcard, yupAudioObjectPropertyElementMain }).value_or (0); - - UInt32 streamLatency = 0; - - if (auto streams = audioObjectGetProperties (parent.deviceID, { kAudioDevicePropertyStreams, scope, yupAudioObjectPropertyElementMain }); ! streams.empty()) - streamLatency = audioObjectGetProperty (streams.front(), { kAudioStreamPropertyLatency, scope, yupAudioObjectPropertyElementMain }).value_or (0); - - return static_cast (deviceLatency + safetyOffset + framesInBuffer + streamLatency); - } - - bool checkTimestampsForDiscontinuity (const AudioTimeStamp* timestamp) noexcept - { - if (channels > 0) - { - jassert (timestamp == nullptr || (((timestamp->mFlags & kAudioTimeStampSampleTimeValid) != 0) && ((timestamp->mFlags & kAudioTimeStampHostTimeValid) != 0))); - - if (exactlyEqual (previousSampleTime, invalidSampleTime)) - previousSampleTime = timestamp != nullptr ? timestamp->mSampleTime : 0.0; - - if (timestamp != nullptr && std::fabs (previousSampleTime - timestamp->mSampleTime) >= 1.0) - { - previousSampleTime = timestamp->mSampleTime; - return true; - } - } - - return false; - } - - //============================================================================== - const bool input; - const int latency; - const int bitDepth; - const StringArray chanNames; - const BigInteger activeChans; - const Array channelInfo; - const int channels = 0; - Float64 previousSampleTime; - - HeapBlock tempBuffers; - - YUP_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (Stream) - }; - - template - static auto getWithDefault (const std::unique_ptr& ptr, Callback&& callback) - { - return ptr != nullptr ? callback (*ptr) : decltype (callback (*ptr)) {}; - } - - template - static auto getWithDefault (const std::unique_ptr& ptr, Value (Stream::* member)) - { - return getWithDefault (ptr, [&] (Stream& s) - { - return s.*member; - }); - } - - static int getLatency (const std::unique_ptr& ptr) { return getWithDefault (ptr, &Stream::latency); } - - static int getBitDepth (const std::unique_ptr& ptr) { return getWithDefault (ptr, &Stream::bitDepth); } - - static int getChannels (const std::unique_ptr& ptr) { return getWithDefault (ptr, &Stream::channels); } - - static int getNumChannelNames (const std::unique_ptr& ptr) { return getWithDefault (ptr, &Stream::chanNames).size(); } - - static String getChannelNames (const std::unique_ptr& ptr) { return getWithDefault (ptr, &Stream::chanNames).joinIntoString (" "); } - - static BigInteger getActiveChannels (const std::unique_ptr& ptr) { return getWithDefault (ptr, &Stream::activeChans); } - - static float** getTempBuffers (const std::unique_ptr& ptr) - { - return getWithDefault (ptr, [] (auto& s) - { - return s.tempBuffers.get(); - }); - } - - //============================================================================== - static constexpr Float64 invalidSampleTime = std::numeric_limits::max(); - - CoreAudioIODevice& owner; - int bitDepth = 32; - std::atomic xruns { 0 }; - Array sampleRates; - Array bufferSizes; - AudioDeviceID deviceID; - std::unique_ptr inStream, outStream; - - AudioWorkgroup audioWorkgroup; - - private: - class ScopedAudioDeviceIOProcID - { - public: - ScopedAudioDeviceIOProcID() = default; - - ScopedAudioDeviceIOProcID (CoreAudioInternal& coreAudio, AudioDeviceID d, AudioDeviceIOProc audioIOProc) - : deviceID (d) - { - if (! coreAudio.OK (AudioDeviceCreateIOProcID (deviceID, audioIOProc, &coreAudio, &proc))) - proc = {}; - } - - ~ScopedAudioDeviceIOProcID() noexcept - { - if (proc != AudioDeviceIOProcID {}) - AudioDeviceDestroyIOProcID (deviceID, proc); - } - - ScopedAudioDeviceIOProcID (ScopedAudioDeviceIOProcID&& other) noexcept - { - swap (other); - } - - ScopedAudioDeviceIOProcID& operator= (ScopedAudioDeviceIOProcID&& other) noexcept - { - ScopedAudioDeviceIOProcID { std::move (other) }.swap (*this); - return *this; - } - - AudioDeviceIOProcID get() const { return proc; } - - private: - void swap (ScopedAudioDeviceIOProcID& other) noexcept - { - std::swap (other.deviceID, deviceID); - std::swap (other.proc, proc); - } - - AudioDeviceID deviceID = {}; - AudioDeviceIOProcID proc = {}; - }; - - //============================================================================== - ScopedAudioDeviceIOProcID scopedProcID; - CoreAudioTimeConversions timeConversions; - AudioIODeviceCallback* callback = nullptr; - CriticalSection callbackLock; - bool audioDeviceStopPending = false; - std::atomic playing { false }; - double sampleRate = 0; - int bufferSize = 0; - HeapBlock audioBuffer; - Atomic callbacksAllowed { 1 }; - - //============================================================================== - void timerCallback() override - { - YUP_COREAUDIOLOG ("Device changed"); - - stopTimer(); - auto oldSampleRate = sampleRate; - auto oldBufferSize = bufferSize; - - if (! updateDetailsFromDevice()) - owner.stopInternal(); - else if ((oldBufferSize != bufferSize || ! approximatelyEqual (oldSampleRate, sampleRate)) && owner.shouldRestartDevice()) - owner.restart(); - } - - void handleAsyncUpdate() override - { - if (owner.deviceType != nullptr) - owner.deviceType->audioDeviceListChanged(); - } - - static OSStatus audioIOProc (AudioDeviceID /*inDevice*/, - [[maybe_unused]] const AudioTimeStamp* inNow, - const AudioBufferList* inInputData, - const AudioTimeStamp* inInputTime, - AudioBufferList* outOutputData, - const AudioTimeStamp* inOutputTime, - void* device) - { - static_cast (device)->audioCallback (inInputTime, inOutputTime, inInputData, outOutputData); - return noErr; - } - - static OSStatus deviceListenerProc (AudioDeviceID /*inDevice*/, - UInt32 numAddresses, - const AudioObjectPropertyAddress* pa, - void* inClientData) - { - auto& intern = *static_cast (inClientData); - - const auto xruns = (int) std::count_if (pa, pa + numAddresses, [] (const AudioObjectPropertyAddress& x) - { - return x.mSelector == kAudioDeviceProcessorOverload; - }); - - intern.xruns.fetch_add (xruns); - - const auto detailsChanged = std::any_of (pa, pa + numAddresses, [] (const AudioObjectPropertyAddress& x) - { - constexpr UInt32 selectors[] { - kAudioDevicePropertyBufferSize, - kAudioDevicePropertyBufferFrameSize, - kAudioDevicePropertyNominalSampleRate, - kAudioDevicePropertyStreamFormat, - kAudioDevicePropertyDeviceIsAlive, - kAudioStreamPropertyPhysicalFormat, - }; - - return std::find (std::begin (selectors), std::end (selectors), x.mSelector) != std::end (selectors); - }); - - const auto requestedRestart = std::any_of (pa, pa + numAddresses, [] (const AudioObjectPropertyAddress& x) - { - constexpr UInt32 selectors[] { - kAudioDevicePropertyDeviceHasChanged, - kAudioObjectPropertyOwnedObjects, - }; - - return std::find (std::begin (selectors), std::end (selectors), x.mSelector) != std::end (selectors); - }); - - if (detailsChanged) - intern.deviceDetailsChanged(); - - if (requestedRestart) - intern.deviceRequestedRestart(); - - return noErr; - } - - //============================================================================== - bool OK (const OSStatus errorCode) const - { - if (errorCode == noErr) - return true; - - const String errorMessage ("CoreAudio error: " + String::toHexString ((int) errorCode)); - YUP_COREAUDIOLOG (errorMessage); - - if (callback != nullptr) - callback->audioDeviceError (errorMessage); - - return false; - } - - YUP_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (CoreAudioInternal) - }; - - //============================================================================== - class CoreAudioIODevice final : public AudioIODevice - , private Timer - { - public: - CoreAudioIODevice (CoreAudioIODeviceType* dt, - const String& deviceName, - AudioDeviceID inputDeviceId, - AudioDeviceID outputDeviceId) - : AudioIODevice (deviceName, "CoreAudio") - , deviceType (dt) - { - internal = [this, &inputDeviceId, &outputDeviceId] - { - if (outputDeviceId == 0 || outputDeviceId == inputDeviceId) - { - jassert (inputDeviceId != 0); - return std::make_unique (*this, inputDeviceId, true, outputDeviceId != 0); - } - - if (inputDeviceId == 0) - return std::make_unique (*this, outputDeviceId, false, true); - - // This used to be just "org.yup.aggregate", but macOS doesn't allow two different instances of an app with - // the same bundle ID to create the same aggregate device UID, even when it's private. - NSString* aggregateDeviceUid = CFBridgingRelease (Uuid().toString().toCFString()); - - // kAudioSubDeviceDriftCompensationMaxQuality has this value but for some reason in Xcode 15 started being - // marked available only since macOS 13.0, even though it's been available since forever (see - // https://github.com/phracker/MacOSX-SDKs/blob/master/MacOSX10.9.sdk/System/Library/Frameworks/CoreAudio.framework/Versions/A/Headers/AudioHardware.h) - // Maybe it's because the enum type changed?... So, just use our own constant with the same value. - static constexpr UInt32 kDriftCompensationMaxQuality = 0x7F; - - // clang-format off - NSDictionary* description = - @{ - @(kAudioAggregateDeviceUIDKey) : aggregateDeviceUid, - @(kAudioAggregateDeviceIsPrivateKey) : @(1), - @(kAudioAggregateDeviceSubDeviceListKey): - @[ - @{ - @(kAudioSubDeviceUIDKey) : getDeviceUID (inputDeviceId), - @(kAudioSubDeviceDriftCompensationKey) : @(1), - @(kAudioSubDeviceDriftCompensationQualityKey) : @(kDriftCompensationMaxQuality), - }, - @{ - @(kAudioSubDeviceUIDKey) : getDeviceUID (outputDeviceId), - @(kAudioSubDeviceDriftCompensationKey) : @(1), - @(kAudioSubDeviceDriftCompensationQualityKey) : @(kDriftCompensationMaxQuality), - }, - ] - }; - // clang-format on - - // Guaranteed available earlier in CoreAudioIODeviceType::createDevice() - OSStatus status = AudioHardwareCreateAggregateDevice ((__bridge CFDictionaryRef) description, &aggregateDeviceID); - return status == noErr ? std::make_unique (*this, aggregateDeviceID, true, true) : nullptr; - }(); - - jassert (internal != nullptr); - - AudioObjectPropertyAddress pa; - pa.mSelector = kAudioObjectPropertySelectorWildcard; - pa.mScope = kAudioObjectPropertyScopeWildcard; - pa.mElement = kAudioObjectPropertyElementWildcard; - - AudioObjectAddPropertyListener (kAudioObjectSystemObject, &pa, hardwareListenerProc, internal.get()); - } - - ~CoreAudioIODevice() override - { - close(); - - AudioObjectPropertyAddress pa; - pa.mSelector = kAudioObjectPropertySelectorWildcard; - pa.mScope = kAudioObjectPropertyScopeWildcard; - pa.mElement = kAudioObjectPropertyElementWildcard; - - AudioObjectRemovePropertyListener (kAudioObjectSystemObject, &pa, hardwareListenerProc, internal.get()); - - if (aggregateDeviceID != 0) - AudioHardwareDestroyAggregateDevice (aggregateDeviceID); - } - - StringArray getOutputChannelNames() override { return internal->outStream != nullptr ? internal->outStream->chanNames : StringArray(); } - - StringArray getInputChannelNames() override { return internal->inStream != nullptr ? internal->inStream->chanNames : StringArray(); } - - bool isOpen() override { return isOpen_; } - - Array getAvailableSampleRates() override { return internal->sampleRates; } - - Array getAvailableBufferSizes() override { return internal->bufferSizes; } - - double getCurrentSampleRate() override { return internal->getSampleRate(); } - - int getCurrentBitDepth() override { return internal->bitDepth; } - - int getCurrentBufferSizeSamples() override { return internal->getBufferSize(); } - - int getXRunCount() const noexcept override { return internal->xruns.load (std::memory_order_relaxed); } - - int getIndexOfDevice (bool asInput) const { return deviceType->getDeviceNames (asInput).indexOf (getName()); } - - int getDefaultBufferSize() override - { - int best = 0; - - for (int i = 0; best < 512 && i < internal->bufferSizes.size(); ++i) - best = internal->bufferSizes.getUnchecked (i); - - if (best == 0) - best = 512; - - return best; - } - - String open (const BigInteger& inputChannels, - const BigInteger& outputChannels, - double sampleRate, - int bufferSizeSamples) override - { - isOpen_ = true; - internal->xruns.store (0); - - inputChannelsRequested = inputChannels; - outputChannelsRequested = outputChannels; - - if (bufferSizeSamples <= 0) - bufferSizeSamples = getDefaultBufferSize(); - - if (sampleRate <= 0) - sampleRate = internal->getNominalSampleRate(); - - lastError = internal->reopen (inputChannels, outputChannels, sampleRate, bufferSizeSamples); - YUP_COREAUDIOLOG ("Opened: " << getName()); - - isOpen_ = lastError.isEmpty(); - - return lastError; - } - - void close() override - { - isOpen_ = false; - internal->stop (false); - } - - BigInteger getActiveOutputChannels() const override { return CoreAudioInternal::getActiveChannels (internal->outStream); } - - BigInteger getActiveInputChannels() const override { return CoreAudioInternal::getActiveChannels (internal->inStream); } - - int getOutputLatencyInSamples() override { return CoreAudioInternal::getLatency (internal->outStream); } - - int getInputLatencyInSamples() override { return CoreAudioInternal::getLatency (internal->inStream); } - - void start (AudioIODeviceCallback* callback) override - { - if (internal->start (callback)) - previousCallback = callback; - } - - void stop() override - { - restartDevice = false; - stopAndGetLastCallback(); - } - - AudioIODeviceCallback* stopAndGetLastCallback() const - { - auto* lastCallback = internal->stop (true); - - if (lastCallback != nullptr) - lastCallback->audioDeviceStopped(); - - return lastCallback; - } - - AudioIODeviceCallback* stopInternal() - { - restartDevice = true; - return stopAndGetLastCallback(); - } - - AudioWorkgroup getWorkgroup() const override - { - return internal->audioWorkgroup; - } - - bool isPlaying() override - { - return internal->isPlaying(); - } - - String getLastError() override - { - return lastError; - } - - void audioDeviceListChanged() - { - if (deviceType != nullptr) - deviceType->audioDeviceListChanged(); - } - - // called by callbacks (possibly off the main thread) - void restart() - { - if (restarter != nullptr) - { - restarter->restartAsync(); - return; - } - - { - const ScopedLock sl (closeLock); - previousCallback = stopInternal(); - } - - startTimer (100); - } - - bool setCurrentSampleRate (double newSampleRate) - { - return internal->setNominalSampleRate (newSampleRate); - } - - void setAsyncRestarter (AsyncRestarter* restarterIn) - { - restarter = restarterIn; - } - - bool shouldRestartDevice() const noexcept { return restartDevice; } - - WeakReference deviceType; - bool hadDiscontinuity; - - private: - std::unique_ptr internal; - AudioDeviceID aggregateDeviceID = 0; - bool isOpen_ = false, restartDevice = true; - String lastError; - AudioIODeviceCallback* previousCallback = nullptr; - AsyncRestarter* restarter = nullptr; - BigInteger inputChannelsRequested, outputChannelsRequested; - CriticalSection closeLock; - - void timerCallback() override - { - stopTimer(); - - stopInternal(); - - internal->updateDetailsFromDevice(); - - open (inputChannelsRequested, outputChannelsRequested, getCurrentSampleRate(), getCurrentBufferSizeSamples()); - start (previousCallback); - } - - static OSStatus hardwareListenerProc (AudioDeviceID /*inDevice*/, - UInt32 numAddresses, - const AudioObjectPropertyAddress* pa, - void* inClientData) - { - const auto detailsChanged = std::any_of (pa, pa + numAddresses, [] (const AudioObjectPropertyAddress& x) - { - return x.mSelector == kAudioHardwarePropertyDevices; - }); - - if (detailsChanged) - static_cast (inClientData)->deviceDetailsChanged(); - - return noErr; - } - - static NSString* getDeviceUID (AudioDeviceID deviceID) - { - CFStringRef uid = nullptr; - UInt32 uidSize = sizeof (uid); - - AudioObjectPropertyAddress pa { kAudioDevicePropertyDeviceUID, kAudioObjectPropertyScopeGlobal, kAudioObjectPropertyElementMaster }; - AudioObjectGetPropertyData (deviceID, &pa, 0, nullptr, &uidSize, &uid); - - return CFBridgingRelease (uid); - } - - YUP_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (CoreAudioIODevice) - }; - - //============================================================================== - class AudioIODeviceCombiner final : public AudioIODevice - , private AsyncRestarter - , private Timer - { - public: - AudioIODeviceCombiner (const String& deviceName, CoreAudioIODeviceType* deviceType, std::unique_ptr&& inputDevice, std::unique_ptr&& outputDevice) - : AudioIODevice (deviceName, "CoreAudio") - , owner (deviceType) - , currentSampleRate (inputDevice->getCurrentSampleRate()) - , currentBufferSize (inputDevice->getCurrentBufferSizeSamples()) - , inputWrapper (*this, std::move (inputDevice), true) - , outputWrapper (*this, std::move (outputDevice), false) - { - if (getAvailableSampleRates().isEmpty()) - lastError = TRANS ("The input and output devices don't share a common sample rate!"); - } - - ~AudioIODeviceCombiner() override - { - close(); - } - - auto getDeviceWrappers() { return std::array { { &inputWrapper, &outputWrapper } }; } - - auto getDeviceWrappers() const { return std::array { { &inputWrapper, &outputWrapper } }; } - - int getIndexOfDevice (bool asInput) const - { - return asInput ? inputWrapper.getIndexOfDevice (true) - : outputWrapper.getIndexOfDevice (false); - } - - StringArray getOutputChannelNames() override { return outputWrapper.getChannelNames(); } - - StringArray getInputChannelNames() override { return inputWrapper.getChannelNames(); } - - BigInteger getActiveOutputChannels() const override { return outputWrapper.getActiveChannels(); } - - BigInteger getActiveInputChannels() const override { return inputWrapper.getActiveChannels(); } - - Array getAvailableSampleRates() override - { - auto commonRates = inputWrapper.getAvailableSampleRates(); - commonRates.removeValuesNotIn (outputWrapper.getAvailableSampleRates()); - - return commonRates; - } - - Array getAvailableBufferSizes() override - { - auto commonSizes = inputWrapper.getAvailableBufferSizes(); - commonSizes.removeValuesNotIn (outputWrapper.getAvailableBufferSizes()); - - return commonSizes; - } - - bool isOpen() override { return active; } - - bool isPlaying() override { return callback != nullptr; } - - double getCurrentSampleRate() override { return currentSampleRate; } - - int getCurrentBufferSizeSamples() override { return currentBufferSize; } - - int getCurrentBitDepth() override - { - return jmin (32, inputWrapper.getCurrentBitDepth(), outputWrapper.getCurrentBitDepth()); - } - - int getDefaultBufferSize() override - { - return jmax (0, inputWrapper.getDefaultBufferSize(), outputWrapper.getDefaultBufferSize()); - } - - AudioWorkgroup getWorkgroup() const override - { - return inputWrapper.getWorkgroup(); - } - - String open (const BigInteger& inputChannels, - const BigInteger& outputChannels, - double sampleRate, - int bufferSize) override - { - inputChannelsRequested = inputChannels; - outputChannelsRequested = outputChannels; - sampleRateRequested = sampleRate; - bufferSizeRequested = bufferSize; - - close(); - active = true; - - if (bufferSize <= 0) - bufferSize = getDefaultBufferSize(); - - if (sampleRate <= 0) - { - auto rates = getAvailableSampleRates(); - - for (int i = 0; i < rates.size() && sampleRate < 44100.0; ++i) - sampleRate = rates.getUnchecked (i); - } - - currentSampleRate = sampleRate; - currentBufferSize = bufferSize; - targetLatency = bufferSize; - - for (auto& d : getDeviceWrappers()) - { - auto err = d->open (d->isInput() ? inputChannels : BigInteger(), - ! d->isInput() ? outputChannels : BigInteger(), - sampleRate, - bufferSize); - - if (err.isNotEmpty()) - { - close(); - lastError = err; - return err; - } - - targetLatency += d->getLatencyInSamples(); - } - - const auto numOuts = outputWrapper.getChannelNames().size(); - - fifo.setSize (numOuts, targetLatency + (bufferSize * 2)); - scratchBuffer.setSize (numOuts, bufferSize); - - return {}; - } - - void close() override - { - stop(); - fifo.clear(); - active = false; - - for (auto& d : getDeviceWrappers()) - d->close(); - } - - void restart() - { - const ScopedLock sl (closeLock); - - AudioIODeviceCallback* cb = previousCallback; - - close(); - - auto newSampleRate = sampleRateRequested; - auto newBufferSize = bufferSizeRequested; - - for (auto& d : getDeviceWrappers()) - { - auto deviceSampleRate = d->getCurrentSampleRate(); - - if (! approximatelyEqual (deviceSampleRate, sampleRateRequested)) - { - if (! getAvailableSampleRates().contains (deviceSampleRate)) - return; - - for (auto& d2 : getDeviceWrappers()) - if (&d2 != &d) - d2->setCurrentSampleRate (deviceSampleRate); - - newSampleRate = deviceSampleRate; - break; - } - } - - for (auto& d : getDeviceWrappers()) - { - auto deviceBufferSize = d->getCurrentBufferSizeSamples(); - - if (deviceBufferSize != bufferSizeRequested) - { - if (! getAvailableBufferSizes().contains (deviceBufferSize)) - return; - - newBufferSize = deviceBufferSize; - break; - } - } - - open (inputChannelsRequested, outputChannelsRequested, newSampleRate, newBufferSize); - - start (cb); - } - - void restartAsync() override - { - { - const ScopedLock sl (closeLock); - - if (active) - { - if (callback != nullptr) - previousCallback = callback; - - close(); - } - } - - startTimer (100); - } - - int getOutputLatencyInSamples() override - { - return targetLatency - getInputLatencyInSamples(); - } - - int getInputLatencyInSamples() override - { - return inputWrapper.getLatencyInSamples(); - } - - void start (AudioIODeviceCallback* newCallback) override - { - const auto shouldStart = [&] - { - const ScopedLock sl (callbackLock); - return callback != newCallback; - }(); - - if (shouldStart) - { - stop(); - fifo.clear(); - reset(); - - { - ScopedErrorForwarder forwarder (*this, newCallback); - - for (auto& d : getDeviceWrappers()) - d->start (d); - - if (! forwarder.encounteredError() && newCallback != nullptr) - newCallback->audioDeviceAboutToStart (this); - else if (lastError.isEmpty()) - lastError = TRANS ("Failed to initialise all requested devices."); - } - - const ScopedLock sl (callbackLock); - previousCallback = callback = newCallback; - } - } - - void stop() override { shutdown ({}); } - - String getLastError() override - { - return lastError; - } - - int getXRunCount() const noexcept override - { - return xruns.load (std::memory_order_relaxed); - } - - private: - static constexpr auto invalidSampleTime = std::numeric_limits::max(); - - WeakReference owner; - CriticalSection callbackLock; - CriticalSection closeLock; - AudioIODeviceCallback* callback = nullptr; - AudioIODeviceCallback* previousCallback = nullptr; - double currentSampleRate = 0; - int currentBufferSize = 0; - bool active = false; - String lastError; - AudioSampleBuffer fifo, scratchBuffer; - int targetLatency = 0; - std::atomic xruns { -1 }; - std::atomic lastValidReadPosition { invalidSampleTime }; - - BigInteger inputChannelsRequested, outputChannelsRequested; - double sampleRateRequested = 44100; - int bufferSizeRequested = 512; - - void timerCallback() override - { - stopTimer(); - - restart(); - } - - void shutdown (const String& error) - { - AudioIODeviceCallback* lastCallback = nullptr; - - { - const ScopedLock sl (callbackLock); - std::swap (callback, lastCallback); - } - - for (auto& d : getDeviceWrappers()) - d->stopInternal(); - - if (lastCallback != nullptr) - { - if (error.isNotEmpty()) - lastCallback->audioDeviceError (error); - else - lastCallback->audioDeviceStopped(); - } - } - - void reset() - { - xruns.store (0); - fifo.clear(); - scratchBuffer.clear(); - - for (auto& d : getDeviceWrappers()) - d->reset(); - } - - // AbstractFifo cannot be used here for two reasons: - // 1) We use absolute timestamps as the fifo's read/write positions. This not only makes the code - // more readable (especially when checking for underruns/overflows) but also simplifies the - // initial setup when actual latency is not known yet until both callbacks have fired. - // 2) AbstractFifo doesn't have the necessary mechanics to recover from underrun/overflow conditions - // in a lock-free and data-race free way. It's great if you don't care (i.e. overwrite and/or - // read stale data) or can abort the operation entirely, but this is not the case here. We - // need bespoke underrun/overflow handling here which fits this use-case. - template - void accessFifo (const uint64_t startPos, const int numChannels, const int numItems, Callback&& operateOnRange) - { - const auto fifoSize = fifo.getNumSamples(); - auto fifoPos = static_cast (startPos % static_cast (fifoSize)); - - for (int pos = 0; pos < numItems;) - { - const auto max = std::min (numItems - pos, fifoSize - fifoPos); - - struct Args - { - int fifoPos, inputPos, nItems, channel; - }; - - for (auto ch = 0; ch < numChannels; ++ch) - operateOnRange (Args { fifoPos, pos, max, ch }); - - fifoPos = (fifoPos + max) % fifoSize; - pos += max; - } - } - - void inputAudioCallback (const float* const* channels, int numChannels, int n, const AudioIODeviceCallbackContext& context) noexcept - { - auto& writePos = inputWrapper.sampleTime; - - { - ScopedLock lock (callbackLock); - - if (callback != nullptr) - { - const auto numActiveOutputChannels = outputWrapper.getActiveChannels().countNumberOfSetBits(); - jassert (numActiveOutputChannels <= scratchBuffer.getNumChannels()); - - callback->audioDeviceIOCallbackWithContext (channels, - numChannels, - scratchBuffer.getArrayOfWritePointers(), - numActiveOutputChannels, - n, - context); - } - else - { - scratchBuffer.clear(); - } - } - - auto currentWritePos = writePos.load(); - const auto nextWritePos = currentWritePos + static_cast (n); - - writePos.compare_exchange_strong (currentWritePos, nextWritePos); - - if (currentWritePos == invalidSampleTime) - return; - - const auto readPos = outputWrapper.sampleTime.load(); - - // check for fifo overflow - if (readPos != invalidSampleTime) - { - // write will overlap previous read - if (readPos > currentWritePos || (currentWritePos + static_cast (n) - readPos) > static_cast (fifo.getNumSamples())) - { - xrun(); - return; - } - } - - accessFifo (currentWritePos, scratchBuffer.getNumChannels(), n, [&] (const auto& args) - { - FloatVectorOperations::copy (fifo.getWritePointer (args.channel, args.fifoPos), - scratchBuffer.getReadPointer (args.channel, args.inputPos), - args.nItems); - }); - - { - auto invalid = invalidSampleTime; - lastValidReadPosition.compare_exchange_strong (invalid, nextWritePos); - } - } - - void outputAudioCallback (float* const* channels, int numChannels, int n) noexcept - { - auto& readPos = outputWrapper.sampleTime; - auto currentReadPos = readPos.load(); - - if (currentReadPos == invalidSampleTime) - return; - - const auto writePos = inputWrapper.sampleTime.load(); - - // check for fifo underrun - if (writePos != invalidSampleTime) - { - if ((currentReadPos + static_cast (n)) > writePos) - { - xrun(); - return; - } - } - - // If there was an xrun, we want to output zeros until we're sure that there's some valid - // input for us to read. - const auto longN = static_cast (n); - const auto nextReadPos = currentReadPos + longN; - const auto validReadPos = lastValidReadPosition.load(); - const auto sanitisedValidReadPos = validReadPos != invalidSampleTime ? validReadPos : nextReadPos; - const auto numZerosToWrite = sanitisedValidReadPos <= currentReadPos - ? 0 - : jmin (longN, sanitisedValidReadPos - currentReadPos); - - for (auto i = 0; i < numChannels; ++i) - std::fill (channels[i], channels[i] + numZerosToWrite, 0.0f); - - accessFifo (currentReadPos + numZerosToWrite, numChannels, static_cast (longN - numZerosToWrite), [&] (const auto& args) - { - FloatVectorOperations::copy (channels[args.channel] + args.inputPos + numZerosToWrite, - fifo.getReadPointer (args.channel, args.fifoPos), - args.nItems); - }); - - // use compare exchange here as we need to avoid the case - // where we overwrite readPos being equal to invalidSampleTime - readPos.compare_exchange_strong (currentReadPos, nextReadPos); - } - - void xrun() noexcept - { - for (auto& d : getDeviceWrappers()) - d->sampleTime.store (invalidSampleTime); - - xruns.fetch_add (1); - } - - void handleAudioDeviceAboutToStart (AudioIODevice* device) - { - const ScopedLock sl (callbackLock); - - auto newSampleRate = device->getCurrentSampleRate(); - auto commonRates = getAvailableSampleRates(); - - if (! commonRates.contains (newSampleRate)) - { - commonRates.sort(); - - if (newSampleRate < commonRates.getFirst() || newSampleRate > commonRates.getLast()) - { - newSampleRate = jlimit (commonRates.getFirst(), commonRates.getLast(), newSampleRate); - } - else - { - for (auto it = commonRates.begin(); it < commonRates.end() - 1; ++it) - { - if (it[0] < newSampleRate && it[1] > newSampleRate) - { - newSampleRate = newSampleRate - it[0] < it[1] - newSampleRate ? it[0] : it[1]; - break; - } - } - } - } - - currentSampleRate = newSampleRate; - bool anySampleRateChanges = false; - - for (auto& d : getDeviceWrappers()) - { - if (! approximatelyEqual (d->getCurrentSampleRate(), currentSampleRate)) - { - d->setCurrentSampleRate (currentSampleRate); - anySampleRateChanges = true; - } - } - - if (anySampleRateChanges && owner != nullptr) - owner->audioDeviceListChanged(); - - if (callback != nullptr) - callback->audioDeviceAboutToStart (device); - } - - void handleAudioDeviceStopped() { shutdown ({}); } - - void handleAudioDeviceError (const String& errorMessage) { shutdown (errorMessage.isNotEmpty() ? errorMessage : String ("unknown")); } - - //============================================================================== - struct DeviceWrapper final : public AudioIODeviceCallback - { - DeviceWrapper (AudioIODeviceCombiner& cd, std::unique_ptr d, bool shouldBeInput) - : owner (cd) - , device (std::move (d)) - , input (shouldBeInput) - { - device->setAsyncRestarter (&owner); - } - - ~DeviceWrapper() override - { - device->close(); - } - - void reset() - { - sampleTime.store (invalidSampleTime); - } - - void audioDeviceIOCallbackWithContext (const float* const* inputChannelData, - int numInputChannels, - float* const* outputChannelData, - int numOutputChannels, - int numSamples, - const AudioIODeviceCallbackContext& context) override - { - if (std::exchange (device->hadDiscontinuity, false)) - owner.xrun(); - - updateSampleTimeFromContext (context); - - if (input) - owner.inputAudioCallback (inputChannelData, numInputChannels, numSamples, context); - else - owner.outputAudioCallback (outputChannelData, numOutputChannels, numSamples); - } - - void audioDeviceAboutToStart (AudioIODevice* d) override { owner.handleAudioDeviceAboutToStart (d); } - - void audioDeviceStopped() override { owner.handleAudioDeviceStopped(); } - - void audioDeviceError (const String& errorMessage) override { owner.handleAudioDeviceError (errorMessage); } - - bool setCurrentSampleRate (double newSampleRate) { return device->setCurrentSampleRate (newSampleRate); } - - StringArray getChannelNames() const { return input ? device->getInputChannelNames() : device->getOutputChannelNames(); } - - BigInteger getActiveChannels() const { return input ? device->getActiveInputChannels() : device->getActiveOutputChannels(); } - - int getLatencyInSamples() const { return input ? device->getInputLatencyInSamples() : device->getOutputLatencyInSamples(); } - - int getIndexOfDevice (bool asInput) const { return device->getIndexOfDevice (asInput); } - - double getCurrentSampleRate() const { return device->getCurrentSampleRate(); } - - int getCurrentBufferSizeSamples() const { return device->getCurrentBufferSizeSamples(); } - - Array getAvailableSampleRates() const { return device->getAvailableSampleRates(); } - - Array getAvailableBufferSizes() const { return device->getAvailableBufferSizes(); } - - int getCurrentBitDepth() const { return device->getCurrentBitDepth(); } - - int getDefaultBufferSize() const { return device->getDefaultBufferSize(); } - - void start (AudioIODeviceCallback* callbackToNotify) const { return device->start (callbackToNotify); } - - AudioIODeviceCallback* stopInternal() const { return device->stopInternal(); } - - void close() const { return device->close(); } - - AudioWorkgroup getWorkgroup() const { return device->getWorkgroup(); } - - String open (const BigInteger& inputChannels, const BigInteger& outputChannels, double sampleRate, int bufferSizeSamples) const - { - return device->open (inputChannels, outputChannels, sampleRate, bufferSizeSamples); - } - - std::uint64_t nsToSampleTime (std::uint64_t ns) const noexcept - { - return static_cast (std::round (static_cast (ns) * device->getCurrentSampleRate() * 1e-9)); - } - - void updateSampleTimeFromContext (const AudioIODeviceCallbackContext& context) noexcept - { - auto callbackSampleTime = context.hostTimeNs != nullptr ? nsToSampleTime (*context.hostTimeNs) : 0; - - if (input) - callbackSampleTime += static_cast (owner.targetLatency); - - auto copy = invalidSampleTime; - - if (sampleTime.compare_exchange_strong (copy, callbackSampleTime) && (! input)) - owner.lastValidReadPosition = invalidSampleTime; - } - - bool isInput() const { return input; } - - std::atomic sampleTime { invalidSampleTime }; - - private: - //============================================================================== - AudioIODeviceCombiner& owner; - std::unique_ptr device; - const bool input; - - //============================================================================== - YUP_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (DeviceWrapper) - }; - - /* If the current AudioIODeviceCombiner::callback is nullptr, it sets itself as the callback - and forwards error related callbacks to the provided callback. */ - class ScopedErrorForwarder final : public AudioIODeviceCallback - { - public: - ScopedErrorForwarder (AudioIODeviceCombiner& ownerIn, AudioIODeviceCallback* cb) - : owner (ownerIn) - , target (cb) - { - const ScopedLock sl (owner.callbackLock); - - if (owner.callback == nullptr) - owner.callback = this; - } - - ~ScopedErrorForwarder() override - { - const ScopedLock sl (owner.callbackLock); - - if (owner.callback == this) - owner.callback = nullptr; - } - - // We only want to be notified about error conditions when the owner's callback is nullptr. - // This class shouldn't be relied on for forwarding this call. - void audioDeviceAboutToStart (AudioIODevice*) override {} - - void audioDeviceStopped() override - { - if (target != nullptr) - target->audioDeviceStopped(); - - // The audio device may stop because it's about to be restarted with new settings. - // Stopping the device doesn't necessarily count as an error. - } - - void audioDeviceError (const String& errorMessage) override - { - owner.lastError = errorMessage; - - if (target != nullptr) - target->audioDeviceError (errorMessage); - - error = true; - } - - bool encounteredError() const { return error; } - - private: - AudioIODeviceCombiner& owner; - AudioIODeviceCallback* target; - bool error = false; - }; - - DeviceWrapper inputWrapper, outputWrapper; - - YUP_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (AudioIODeviceCombiner) - }; - - //============================================================================== - class CoreAudioIODeviceType final : public AudioIODeviceType - , private AsyncUpdater - { - public: - CoreAudioIODeviceType() - : AudioIODeviceType ("CoreAudio") - { - AudioObjectPropertyAddress pa; - pa.mSelector = kAudioHardwarePropertyDevices; - pa.mScope = kAudioObjectPropertyScopeWildcard; - pa.mElement = kAudioObjectPropertyElementWildcard; - - AudioObjectAddPropertyListener (kAudioObjectSystemObject, &pa, hardwareListenerProc, this); - } - - ~CoreAudioIODeviceType() override - { - cancelPendingUpdate(); - - AudioObjectPropertyAddress pa; - pa.mSelector = kAudioHardwarePropertyDevices; - pa.mScope = kAudioObjectPropertyScopeWildcard; - pa.mElement = kAudioObjectPropertyElementWildcard; - - AudioObjectRemovePropertyListener (kAudioObjectSystemObject, &pa, hardwareListenerProc, this); - } - - //============================================================================== - void scanForDevices() override - { - hasScanned = true; - - inputDeviceNames.clear(); - outputDeviceNames.clear(); - inputIds.clear(); - outputIds.clear(); - - auto audioDevices = audioObjectGetProperties (kAudioObjectSystemObject, { kAudioHardwarePropertyDevices, kAudioObjectPropertyScopeWildcard, yupAudioObjectPropertyElementMain }); - - for (const auto audioDevice : audioDevices) - { - if (const auto optionalName = audioObjectGetProperty (audioDevice, { kAudioDevicePropertyDeviceNameCFString, kAudioObjectPropertyScopeWildcard, yupAudioObjectPropertyElementMain })) - { - if (const CFUniquePtr name { *optionalName }) - { - const auto nameString = String::fromCFString (name.get()); - - if (const auto numIns = getNumChannels (audioDevice, true); numIns > 0) - { - inputDeviceNames.add (nameString); - inputIds.add (audioDevice); - } - - if (const auto numOuts = getNumChannels (audioDevice, false); numOuts > 0) - { - outputDeviceNames.add (nameString); - outputIds.add (audioDevice); - } - } - } - } - - inputDeviceNames.appendNumbersToDuplicates (false, true); - outputDeviceNames.appendNumbersToDuplicates (false, true); - } - - StringArray getDeviceNames (bool wantInputNames) const override - { - jassert (hasScanned); // need to call scanForDevices() before doing this - - return wantInputNames ? inputDeviceNames - : outputDeviceNames; - } - - int getDefaultDeviceIndex (bool forInput) const override - { - jassert (hasScanned); // need to call scanForDevices() before doing this - - // if they're asking for any input channels at all, use the default input, so we - // get the built-in mic rather than the built-in output with no inputs.. - - AudioObjectPropertyAddress pa; - auto selector = forInput ? kAudioHardwarePropertyDefaultInputDevice - : kAudioHardwarePropertyDefaultOutputDevice; - pa.mScope = kAudioObjectPropertyScopeWildcard; - pa.mElement = yupAudioObjectPropertyElementMain; - - if (auto deviceID = audioObjectGetProperty (kAudioObjectSystemObject, { selector, kAudioObjectPropertyScopeWildcard, yupAudioObjectPropertyElementMain })) - { - auto& ids = forInput ? inputIds : outputIds; - - if (auto it = std::find (ids.begin(), ids.end(), deviceID); it != ids.end()) - return static_cast (std::distance (ids.begin(), it)); - } - - return 0; - } - - int getIndexOfDevice (AudioIODevice* device, bool asInput) const override - { - jassert (hasScanned); // need to call scanForDevices() before doing this - - if (auto* d = dynamic_cast (device)) - return d->getIndexOfDevice (asInput); - - if (auto* d = dynamic_cast (device)) - return d->getIndexOfDevice (asInput); - - return -1; - } - - bool hasSeparateInputsAndOutputs() const override { return true; } - - AudioIODevice* createDevice (const String& outputDeviceName, - const String& inputDeviceName) override - { - jassert (hasScanned); // need to call scanForDevices() before doing this - - auto inputIndex = inputDeviceNames.indexOf (inputDeviceName); - auto outputIndex = outputDeviceNames.indexOf (outputDeviceName); - - auto inputDeviceID = inputIds[inputIndex]; - auto outputDeviceID = outputIds[outputIndex]; - - if (inputDeviceID == 0 && outputDeviceID == 0) - return nullptr; - - auto combinedName = outputDeviceName.isEmpty() ? inputDeviceName - : outputDeviceName; - - // Newer Apple platforms can create aggregate audio devices - // clang-format off - bool canCreateAppleAggregateDevice = [] - { - if (@available (macOS 10.9, iOS 7, *)) - return true; - return false; - }(); - // clang-format on - - if (inputDeviceID == outputDeviceID || canCreateAppleAggregateDevice) - return std::make_unique (this, combinedName, inputDeviceID, outputDeviceID).release(); - - auto in = inputDeviceID != 0 ? std::make_unique (this, inputDeviceName, inputDeviceID, 0) - : nullptr; - - auto out = outputDeviceID != 0 ? std::make_unique (this, outputDeviceName, 0, outputDeviceID) - : nullptr; - - if (in == nullptr) - return out.release(); - if (out == nullptr) - return in.release(); - - auto combo = std::make_unique (combinedName, this, std::move (in), std::move (out)); - return combo.release(); - } - - void audioDeviceListChanged() - { - scanForDevices(); - callDeviceChangeListeners(); - } - - //============================================================================== - private: - StringArray inputDeviceNames, outputDeviceNames; - Array inputIds, outputIds; - - bool hasScanned = false; - - void handleAsyncUpdate() override - { - audioDeviceListChanged(); - } - - static int getNumChannels (AudioDeviceID deviceID, bool input) - { - int total = 0; - - if (auto bufList = audioObjectGetProperty (deviceID, { kAudioDevicePropertyStreamConfiguration, CoreAudioInternal::getScope (input), yupAudioObjectPropertyElementMain })) - { - auto numStreams = (int) bufList->mNumberBuffers; - - for (int i = 0; i < numStreams; ++i) - total += bufList->mBuffers[i].mNumberChannels; - } - - return total; - } - - static OSStatus hardwareListenerProc (AudioDeviceID, UInt32, const AudioObjectPropertyAddress*, void* clientData) - { - static_cast (clientData)->triggerAsyncUpdate(); - return noErr; - } - - YUP_DECLARE_WEAK_REFERENCEABLE (CoreAudioIODeviceType) - YUP_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (CoreAudioIODeviceType) - }; -}; - -#undef YUP_COREAUDIOLOG - -} // namespace yup +/* + ============================================================================== + + This file is part of the YUP library. + Copyright (c) 2024 - kunitoki@gmail.com + + YUP is an open source library subject to open-source licensing. + + The code included in this file is provided under the terms of the ISC license + http://www.isc.org/downloads/software-support-policy/isc-license. Permission + to use, copy, modify, and/or distribute this software for any purpose with or + without fee is hereby granted provided that the above copyright notice and + this permission notice appear in all copies. + + YUP IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER + EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE + DISCLAIMED. + + ============================================================================== + + This file is part of the JUCE library. + Copyright (c) 2022 - Raw Material Software Limited + + JUCE is an open source library subject to commercial or open-source + licensing. + + The code included in this file is provided under the terms of the ISC license + http://www.isc.org/downloads/software-support-policy/isc-license. Permission + To use, copy, modify, and/or distribute this software for any purpose with or + without fee is hereby granted provided that the above copyright notice and + this permission notice appear in all copies. + + JUCE IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER + EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE + DISCLAIMED. + + ============================================================================== +*/ + +namespace yup +{ + +YUP_BEGIN_IGNORE_WARNINGS_GCC_LIKE ("-Wnonnull") + +//============================================================================== +constexpr auto yupAudioObjectPropertyElementMain = +#if defined(MAC_OS_VERSION_12_0) + kAudioObjectPropertyElementMain; +#else + kAudioObjectPropertyElementMaster; +#endif + +//============================================================================== +enum class PlaybackDirection : std::size_t +{ + input = 0, + output = 1 +}; + +static constexpr std::size_t toDirectionIndex (PlaybackDirection d) noexcept +{ + return static_cast (d); +} + +static constexpr std::array getAllPlaybackDirections() noexcept +{ + return { PlaybackDirection::input, PlaybackDirection::output }; +} + +static constexpr AudioObjectPropertyScope toAudioPropertyScope (PlaybackDirection direction) noexcept +{ + return direction == PlaybackDirection::input ? kAudioDevicePropertyScopeInput + : kAudioDevicePropertyScopeOutput; +} + +//============================================================================== +class PropertyAddress +{ +public: + explicit PropertyAddress (AudioObjectPropertySelector selector) noexcept + : PropertyAddress (selector, kAudioObjectPropertyScopeGlobal, yupAudioObjectPropertyElementMain) + { + } + + PropertyAddress (AudioObjectPropertySelector selector, AudioObjectPropertyScope scope) noexcept + : PropertyAddress (selector, scope, yupAudioObjectPropertyElementMain) + { + } + + PropertyAddress (AudioObjectPropertySelector selector, AudioObjectPropertyScope scope, AudioObjectPropertyElement element) noexcept + : address { selector, scope, element } + { + } + + PropertyAddress (AudioObjectPropertySelector selector, PlaybackDirection direction) noexcept + : PropertyAddress (selector, toAudioPropertyScope (direction), yupAudioObjectPropertyElementMain) + { + } + + PropertyAddress (AudioObjectPropertySelector selector, PlaybackDirection direction, AudioObjectPropertyElement element) noexcept + : PropertyAddress (selector, toAudioPropertyScope (direction), element) + { + } + + const AudioObjectPropertyAddress* get() const noexcept { return &address; } + + operator AudioObjectPropertyAddress() const noexcept { return address; } + +private: + AudioObjectPropertyAddress address; +}; + +//============================================================================== +class PropertyListener +{ +public: + using Callback = std::function; + + PropertyListener (AudioObjectID objectIdIn, + AudioObjectPropertySelector selector, + AudioObjectPropertyScope scope, + Callback callbackIn) + : objectId (objectIdIn) + , address (selector, scope, kAudioObjectPropertyElementWildcard) + , callback (std::move (callbackIn)) + { + if (objectId == kAudioObjectUnknown) + return; + + AudioObjectAddPropertyListener (objectId, address.get(), listenerCallback, this); + } + + ~PropertyListener() + { + if (objectId == kAudioObjectUnknown) + return; + + AudioObjectRemovePropertyListener (objectId, address.get(), listenerCallback, this); + } + +private: + static OSStatus listenerCallback (AudioObjectID, + UInt32 numAddresses, + const AudioObjectPropertyAddress* addrs, + void* clientData) + { + static_cast (clientData)->callback (numAddresses, addrs); + return noErr; + } + + AudioObjectID objectId = kAudioObjectUnknown; + PropertyAddress address; + Callback callback; + + YUP_DECLARE_NON_COPYABLE (PropertyListener) + YUP_DECLARE_NON_MOVEABLE (PropertyListener) +}; + +//============================================================================== +class AudioObject +{ +public: + explicit AudioObject (AudioObjectID objectIdIn) noexcept + : objectId (objectIdIn) + { + } + + AudioObject() noexcept = default; + AudioObject (const AudioObject&) noexcept = default; + AudioObject (AudioObject&&) noexcept = default; + AudioObject& operator= (const AudioObject&) noexcept = default; + AudioObject& operator= (AudioObject&&) noexcept = default; + + AudioObjectID getId() const noexcept { return objectId; } + + bool isValid() const noexcept { return objectId != kAudioObjectUnknown; } + + bool operator== (const AudioObject& other) const noexcept { return objectId == other.objectId; } + + bool operator!= (const AudioObject& other) const noexcept { return ! (*this == other); } + + template + std::optional getProperty (PropertyAddress address) const + { + if (! hasProperty (address)) + return {}; + + UInt32 size = sizeof (PropertyType); + PropertyType value {}; + + if (AudioObjectGetPropertyData (objectId, address.get(), 0, nullptr, &size, &value) != noErr) + return {}; + + return value; + } + + template + PropertyType getPropertyOrDefault (PropertyAddress address, PropertyType defaultValue = {}) const + { + return getProperty (address).value_or (defaultValue); + } + + template + std::vector getPropertyArray (PropertyAddress address) const + { + if (! hasProperty (address)) + return {}; + + UInt32 size = 0; + + if (AudioObjectGetPropertyDataSize (objectId, address.get(), 0, nullptr, &size) != noErr) + return {}; + + jassert ((size % sizeof (PropertyType)) == 0); + std::vector result (size / sizeof (PropertyType)); + + if (AudioObjectGetPropertyData (objectId, address.get(), 0, nullptr, &size, result.data()) != noErr) + return {}; + + return result; + } + + template + bool setProperty (PropertyAddress address, const PropertyType& value) + { + if (! hasProperty (address)) + return false; + + Boolean isSettable = NO; + + if (AudioObjectIsPropertySettable (objectId, address.get(), &isSettable) != noErr || ! isSettable) + return false; + + return AudioObjectSetPropertyData (objectId, address.get(), 0, nullptr, sizeof (PropertyType), &value) == noErr; + } + + std::unique_ptr createPropertyListener (AudioObjectPropertySelector selector, + AudioObjectPropertyScope scope, + PropertyListener::Callback callback) + { + return std::make_unique (objectId, selector, scope, std::move (callback)); + } + +private: + bool hasProperty (PropertyAddress address) const noexcept + { + return isValid() && AudioObjectHasProperty (objectId, address.get()); + } + + AudioObjectID objectId = kAudioObjectUnknown; +}; + +static_assert (sizeof (AudioObject) == sizeof (AudioObjectID)); + +//============================================================================== +class AudioStream : public AudioObject +{ +public: + using AudioObject::AudioObject; + + int getLatency() const + { + return static_cast (getPropertyOrDefault (PropertyAddress (kAudioStreamPropertyLatency))); + } + + int getBitDepth() const + { + return static_cast (getPropertyOrDefault (PropertyAddress (kAudioStreamPropertyPhysicalFormat)).mBitsPerChannel); + } +}; + +static_assert (sizeof (AudioStream) == sizeof (AudioObject)); + +//============================================================================== +static String stringFromRetainedCFString (CFStringRef string) +{ + if (string == nullptr) + return {}; + + const CFUniquePtr holder { string }; + return String::fromCFString (holder.get()); +} + +//============================================================================== +class AudioDevice : public AudioObject +{ + static constexpr AudioObjectPropertySelector mainVolumeSelector = +#if defined(MAC_OS_VERSION_12_0) + kAudioHardwareServiceDeviceProperty_VirtualMainVolume; +#else + kAudioHardwareServiceDeviceProperty_VirtualMasterVolume; +#endif + +public: + using AudioObject::AudioObject; + + String getName() const + { + return stringFromRetainedCFString (getProperty (PropertyAddress (kAudioDevicePropertyDeviceNameCFString)).value_or (nullptr)); + } + + String getUid() const + { + return stringFromRetainedCFString (getProperty (PropertyAddress (kAudioDevicePropertyDeviceUID)).value_or (nullptr)); + } + + double getSampleRate() const + { + return static_cast (getPropertyOrDefault (PropertyAddress (kAudioDevicePropertyNominalSampleRate))); + } + + bool requestSampleRate (double newRate) + { + return setProperty (PropertyAddress (kAudioDevicePropertyNominalSampleRate), static_cast (newRate)); + } + + int getBufferSize() const + { + return static_cast (getPropertyOrDefault (PropertyAddress (kAudioDevicePropertyBufferFrameSize, kAudioObjectPropertyScopeWildcard))); + } + + bool requestBufferSize (int newSize) + { + return setProperty (PropertyAddress (kAudioDevicePropertyBufferFrameSize), static_cast (newSize)); + } + + int getNumChannels (PlaybackDirection direction) const + { + if (! isValid()) + return 0; + + const PropertyAddress address { kAudioDevicePropertyStreamConfiguration, direction }; + + UInt32 size = 0; + + if (! AudioObjectHasProperty (getId(), address.get())) + return 0; + + if (AudioObjectGetPropertyDataSize (getId(), address.get(), 0, nullptr, &size) != noErr || size == 0) + return 0; + + std::vector storage (size); + const auto* bufList = reinterpret_cast (storage.data()); + + if (AudioObjectGetPropertyData (getId(), address.get(), 0, nullptr, &size, storage.data()) != noErr) + return 0; + + int total = 0; + for (UInt32 i = 0; i < bufList->mNumberBuffers; ++i) + total += static_cast (bufList->mBuffers[i].mNumberChannels); + return total; + } + + std::vector getStreams (PlaybackDirection direction) const + { + const auto ids = getPropertyArray (PropertyAddress (kAudioDevicePropertyStreams, direction)); + std::vector streams; + streams.reserve (ids.size()); + for (const auto id : ids) + streams.emplace_back (id); + return streams; + } + + int getLatency (PlaybackDirection direction) const + { + return static_cast (getPropertyOrDefault (PropertyAddress (kAudioDevicePropertyLatency, direction))); + } + + int getSafetyOffset (PlaybackDirection direction) const + { + return static_cast (getPropertyOrDefault (PropertyAddress (kAudioDevicePropertySafetyOffset, direction))); + } + + int getStreamLatency (PlaybackDirection direction) const + { + const auto streams = getStreams (direction); + return streams.empty() ? 0 : streams.front().getLatency(); + } + + int getBitDepth() const + { + for (auto direction : getAllPlaybackDirections()) + for (auto stream : getStreams (direction)) + if (const auto depth = stream.getBitDepth(); depth > 0) + return depth; + + return 24; + } + + String getChannelName (PlaybackDirection direction, int index) const + { + const auto element = static_cast (index + 1); + return stringFromRetainedCFString (getProperty (PropertyAddress (kAudioObjectPropertyElementName, direction, element)).value_or (nullptr)); + } + + float getMainVolume() const + { + return getPropertyOrDefault (PropertyAddress (mainVolumeSelector)); + } + + bool setMainVolume (float newVolume) + { + return setProperty (PropertyAddress (mainVolumeSelector), static_cast (newVolume)); + } + + bool isMuted() const + { + return getPropertyOrDefault (PropertyAddress (kAudioDevicePropertyMute, kAudioDevicePropertyScopeOutput)) != 0; + } + + bool setMute (bool mute) + { + return setProperty (PropertyAddress (kAudioDevicePropertyMute, kAudioDevicePropertyScopeOutput), static_cast (mute ? 1 : 0)); + } + + bool isAlive() const + { + return getPropertyOrDefault (PropertyAddress (kAudioDevicePropertyDeviceIsAlive, kAudioObjectPropertyScopeWildcard)) != 0; + } + + bool isAggregateDevice() const + { + return getPropertyOrDefault (PropertyAddress (kAudioObjectPropertyClass)) == kAudioAggregateDeviceClassID; + } + + AudioWorkgroup getAudioWorkgroup() const + { +#if YUP_AUDIOWORKGROUP_TYPES_AVAILABLE + if (auto handle = getProperty (PropertyAddress (kAudioDevicePropertyIOThreadOSWorkgroup, kAudioObjectPropertyScopeWildcard))) + { + os_workgroup_t workgroup = (__bridge_transfer os_workgroup_t) * handle; + return makeRealAudioWorkgroup (workgroup); + } +#endif + return {}; + } + + std::unique_ptr createPropertyListener (AudioObjectPropertySelector selector, + PropertyListener::Callback callback) + { + return AudioObject::createPropertyListener (selector, kAudioObjectPropertyScopeWildcard, std::move (callback)); + } +}; + +static_assert (sizeof (AudioDevice) == sizeof (AudioObject)); + +//============================================================================== +class SystemObject final : public AudioObject +{ +public: + SystemObject() noexcept + : AudioObject (kAudioObjectSystemObject) + { + } + + AudioDevice getDefaultDevice (PlaybackDirection direction) const + { + static constexpr AudioObjectPropertySelector selectors[] { + kAudioHardwarePropertyDefaultInputDevice, + kAudioHardwarePropertyDefaultOutputDevice + }; + return AudioDevice (getPropertyOrDefault (PropertyAddress (selectors[toDirectionIndex (direction)]))); + } + + std::vector getAudioDevices() const + { + const auto ids = getPropertyArray (PropertyAddress (kAudioHardwarePropertyDevices, kAudioObjectPropertyScopeWildcard)); + std::vector devices; + devices.reserve (ids.size()); + for (const auto id : ids) + devices.emplace_back (id); + return devices; + } + + AudioDevice translateUidToDevice (const String& uid) const + { + if (uid.isEmpty()) + return AudioDevice (kAudioObjectUnknown); + + const CFUniquePtr uidString { uid.toCFString() }; + auto* uidRef = uidString.get(); + AudioDeviceID result = kAudioObjectUnknown; + UInt32 resultSize = sizeof (result); + const PropertyAddress address { kAudioHardwarePropertyTranslateUIDToDevice }; + + AudioObjectGetPropertyData (kAudioObjectSystemObject, address.get(), sizeof (uidRef), &uidRef, &resultSize, &result); + return AudioDevice (result); + } +}; + +//============================================================================== +class AggregateAudioDevice : public AudioDevice +{ +public: + using AudioDevice::AudioDevice; + + std::vector getSubDevices() const + { + const auto ids = getPropertyArray (PropertyAddress (kAudioAggregateDevicePropertyActiveSubDeviceList)); + std::vector devices; + devices.reserve (ids.size()); + for (const auto id : ids) + devices.emplace_back (id); + return devices; + } + + String getClockingDeviceUid() const + { + constexpr auto mainSubDeviceSelector = +#if defined(MAC_OS_VERSION_12_0) + kAudioAggregateDevicePropertyMainSubDevice; +#else + kAudioAggregateDevicePropertyMasterSubDevice; +#endif + + for (const auto selector : { (AudioObjectPropertySelector) kAudioAggregateDevicePropertyClockDevice, (AudioObjectPropertySelector) mainSubDeviceSelector }) + { + const auto uid = stringFromRetainedCFString (getProperty (PropertyAddress (selector)).value_or (nullptr)); + if (uid.isNotEmpty()) + return uid; + } + + const auto subs = getSubDevices(); + return subs.empty() ? String {} : subs.front().getUid(); + } +}; + +static_assert (sizeof (AggregateAudioDevice) == sizeof (AudioObject)); + +//============================================================================== +class ManagedAudioBufferList final : public AudioBufferList +{ +public: + struct Deleter + { + void operator() (ManagedAudioBufferList* p) const + { + if (p != nullptr) + p->~ManagedAudioBufferList(); + + delete[] reinterpret_cast (p); + } + }; + + using Ref = std::unique_ptr; + + //============================================================================== + static Ref create (std::size_t numBuffers) + { + static_assert (alignof (ManagedAudioBufferList) <= alignof (std::max_align_t)); + + if (std::unique_ptr storage { new std::byte[storageSizeForNumBuffers (numBuffers)] }) + return Ref { new (storage.release()) ManagedAudioBufferList (numBuffers) }; + + return nullptr; + } + + //============================================================================== + static std::size_t storageSizeForNumBuffers (std::size_t numBuffers) noexcept + { + return audioBufferListHeaderSize + (numBuffers * sizeof (::AudioBuffer)); + } + + static std::size_t numBuffersForStorageSize (std::size_t bytes) noexcept + { + bytes -= audioBufferListHeaderSize; + + // storage size ends between to buffers in AudioBufferList + jassert ((bytes % sizeof (::AudioBuffer)) == 0); + + return bytes / sizeof (::AudioBuffer); + } + +private: + // Do not call the base constructor here as this will zero-initialize the first buffer, + // for which no storage may be available though (when numBuffers == 0). + explicit ManagedAudioBufferList (std::size_t numBuffers) + { + mNumberBuffers = static_cast (numBuffers); + } + + static constexpr auto audioBufferListHeaderSize = sizeof (AudioBufferList) - sizeof (::AudioBuffer); + + YUP_DECLARE_NON_COPYABLE (ManagedAudioBufferList) + YUP_DECLARE_NON_MOVEABLE (ManagedAudioBufferList) +}; + +//============================================================================== +struct IgnoreUnused +{ + template + void operator() (Ts&&...) const + { + } +}; + +template +static auto getDataPtrAndSize (T& t) +{ + static_assert (std::is_standard_layout_v); + return std::make_tuple (&t, (UInt32) sizeof (T)); +} + +static auto getDataPtrAndSize (ManagedAudioBufferList::Ref& t) +{ + const auto size = t.get() != nullptr + ? ManagedAudioBufferList::storageSizeForNumBuffers (t->mNumberBuffers) + : 0; + return std::make_tuple (t.get(), (UInt32) size); +} + +//============================================================================== +[[nodiscard]] static bool audioObjectHasProperty (AudioObjectID objectID, const AudioObjectPropertyAddress address) +{ + return objectID != kAudioObjectUnknown && AudioObjectHasProperty (objectID, &address); +} + +template +[[nodiscard]] static auto audioObjectGetProperty (AudioObjectID objectID, + const AudioObjectPropertyAddress address, + OnError&& onError = {}) +{ + using Result = std::conditional_t, ManagedAudioBufferList::Ref, std::optional>; + + if (! audioObjectHasProperty (objectID, address)) + return Result {}; + + auto result = [&] + { + if constexpr (std::is_same_v) + { + UInt32 size {}; + + if (auto status = AudioObjectGetPropertyDataSize (objectID, &address, 0, nullptr, &size); status != noErr) + { + onError (status); + return Result {}; + } + + return ManagedAudioBufferList::create (ManagedAudioBufferList::numBuffersForStorageSize (size)); + } + else + { + return T {}; + } + }(); + + auto [ptr, size] = getDataPtrAndSize (result); + + if (size == 0) + return Result {}; + + if (auto status = AudioObjectGetPropertyData (objectID, &address, 0, nullptr, &size, ptr); status != noErr) + { + onError (status); + return Result {}; + } + + return Result { std::move (result) }; +} + +template +static bool audioObjectSetProperty (AudioObjectID objectID, + const AudioObjectPropertyAddress address, + const T value, + OnError&& onError = {}) +{ + if (! audioObjectHasProperty (objectID, address)) + return false; + + Boolean isSettable = NO; + if (auto status = AudioObjectIsPropertySettable (objectID, &address, &isSettable); status != noErr) + { + onError (status); + return false; + } + + if (! isSettable) + return false; + + if (auto status = AudioObjectSetPropertyData (objectID, &address, 0, nullptr, static_cast (sizeof (T)), &value); status != noErr) + { + onError (status); + return false; + } + + return true; +} + +template +[[nodiscard]] static std::vector audioObjectGetProperties (AudioObjectID objectID, + const AudioObjectPropertyAddress address, + OnError&& onError = {}) +{ + if (! audioObjectHasProperty (objectID, address)) + return {}; + + UInt32 size {}; + + if (auto status = AudioObjectGetPropertyDataSize (objectID, &address, 0, nullptr, &size); status != noErr) + { + onError (status); + return {}; + } + + // If this is hit, the number of results is not integral, and the following + // AudioObjectGetPropertyData will probably write past the end of the result buffer. + jassert ((size % sizeof (T)) == 0); + std::vector result (size / sizeof (T)); + + if (auto status = AudioObjectGetPropertyData (objectID, &address, 0, nullptr, &size, result.data()); status != noErr) + { + onError (status); + return {}; + } + + return result; +} + +static AudioObjectPropertyScope getAudioDevicePropertyScope (bool input) noexcept +{ + return toAudioPropertyScope (input ? PlaybackDirection::input : PlaybackDirection::output); +} + +constexpr auto yupPrivateAggregateDeviceNamePrefix = "YUP Aggregate "; +constexpr auto yupPrivateAggregateDevicePIDMarker = "pid="; + +static String createPrivateAggregateDeviceName (const String& deviceName) +{ + return String (yupPrivateAggregateDeviceNamePrefix) + yupPrivateAggregateDevicePIDMarker + String ((int) ::getpid()) + " " + deviceName; +} + +static bool isPrivateAggregateDeviceName (const String& name) +{ + return name.startsWith (yupPrivateAggregateDeviceNamePrefix); +} + +static pid_t getPrivateAggregateDeviceProcessID (const String& name) +{ + if (! isPrivateAggregateDeviceName (name)) + return 0; + + const auto suffix = name.fromFirstOccurrenceOf (yupPrivateAggregateDeviceNamePrefix, false, false).trimStart(); + if (! suffix.startsWith (yupPrivateAggregateDevicePIDMarker)) + return 0; + + const auto pidAndName = suffix.fromFirstOccurrenceOf (yupPrivateAggregateDevicePIDMarker, false, false); + const auto pidString = pidAndName.initialSectionContainingOnly ("0123456789"); + + if (pidString.isEmpty()) + return 0; + + const auto numDigits = pidString.length(); + if (pidAndName.length() <= numDigits || ! CharacterFunctions::isWhitespace (pidAndName[numDigits])) + return 0; + + return (pid_t) pidString.getIntValue(); +} + +static int getDirectionIndex (bool input) noexcept +{ + return static_cast (toDirectionIndex (input ? PlaybackDirection::input : PlaybackDirection::output)); +} + +static String audioObjectGetStringProperty (AudioObjectID objectID, PropertyAddress address) +{ + return stringFromRetainedCFString (audioObjectGetProperty (objectID, address).value_or (nullptr)); +} + +static String audioObjectGetStringProperty (AudioObjectID objectID, AudioObjectPropertySelector selector) +{ + return audioObjectGetStringProperty (objectID, PropertyAddress (selector)); +} + +static String getAudioDeviceUID (AudioDeviceID deviceID) +{ + return audioObjectGetStringProperty (deviceID, kAudioDevicePropertyDeviceUID); +} + +static String getAudioDeviceName (AudioDeviceID deviceID) +{ + return audioObjectGetStringProperty (deviceID, kAudioDevicePropertyDeviceNameCFString); +} + +static AudioDeviceID getAudioDeviceIDForUID (const String& uid) +{ + if (uid.isEmpty()) + return kAudioObjectUnknown; + + const CFUniquePtr uidString { uid.toCFString() }; + auto* uidRef = uidString.get(); + AudioDeviceID result = kAudioObjectUnknown; + UInt32 resultSize = sizeof (result); + const PropertyAddress address { kAudioHardwarePropertyTranslateUIDToDevice }; + + if (AudioObjectGetPropertyData (kAudioObjectSystemObject, address.get(), sizeof (uidRef), &uidRef, &resultSize, &result) != noErr) + return kAudioObjectUnknown; + + return result; +} + +static bool isAggregateAudioDevice (AudioDeviceID deviceID) +{ + return audioObjectGetProperty (deviceID, PropertyAddress (kAudioObjectPropertyClass)).value_or (0) == kAudioAggregateDeviceClassID; +} + +static int getDirectNumChannelsForDevice (AudioDeviceID deviceID, bool input) +{ + int total = 0; + + if (auto bufList = audioObjectGetProperty (deviceID, PropertyAddress (kAudioDevicePropertyStreamConfiguration, getAudioDevicePropertyScope (input)))) + { + const auto numStreams = static_cast (bufList->mNumberBuffers); + + for (int i = 0; i < numStreams; ++i) + total += static_cast (bufList->mBuffers[i].mNumberChannels); + } + + return total; +} + +static std::vector getAggregateSubDeviceIDs (AudioDeviceID deviceID) +{ + return audioObjectGetProperties (deviceID, PropertyAddress (kAudioAggregateDevicePropertyActiveSubDeviceList)); +} + +static void addAudioDeviceIDIfMissing (std::vector& deviceIDs, AudioDeviceID deviceID) +{ + if (deviceID == kAudioObjectUnknown || deviceID == 0) + return; + + if (std::find (deviceIDs.begin(), deviceIDs.end(), deviceID) == deviceIDs.end()) + deviceIDs.push_back (deviceID); +} + +static void appendFlattenedAudioDeviceIDs (std::vector& result, AudioDeviceID deviceID, int depth = 0) +{ + if (deviceID == kAudioObjectUnknown || deviceID == 0) + return; + + if (depth > 8 || ! isAggregateAudioDevice (deviceID)) + { + addAudioDeviceIDIfMissing (result, deviceID); + return; + } + + auto subDeviceIDs = getAggregateSubDeviceIDs (deviceID); + + if (subDeviceIDs.empty()) + { + addAudioDeviceIDIfMissing (result, deviceID); + return; + } + + for (const auto subDeviceID : subDeviceIDs) + appendFlattenedAudioDeviceIDs (result, subDeviceID, depth + 1); +} + +static std::vector getFlattenedAudioDeviceIDs (AudioDeviceID deviceID) +{ + std::vector result; + appendFlattenedAudioDeviceIDs (result, deviceID); + return result; +} + +static int getNumChannelsForAudioDevice (AudioDeviceID deviceID, bool input) +{ + if (isAggregateAudioDevice (deviceID)) + { + auto total = 0; + + for (const auto subDeviceID : getFlattenedAudioDeviceIDs (deviceID)) + total += getDirectNumChannelsForDevice (subDeviceID, input); + + if (total > 0) + return total; + } + + return getDirectNumChannelsForDevice (deviceID, input); +} + +struct AggregateSubDeviceChannelGroup +{ + String name; + int remainingChannels = 0; +}; + +static std::vector getAggregateSubDeviceChannelGroups (AudioDeviceID deviceID, bool input) +{ + std::vector result; + + if (! isAggregateAudioDevice (deviceID)) + return result; + + for (const auto subDeviceID : getFlattenedAudioDeviceIDs (deviceID)) + { + const auto numChannels = getDirectNumChannelsForDevice (subDeviceID, input); + + if (numChannels > 0) + result.push_back ({ getAudioDeviceName (subDeviceID), numChannels }); + } + + return result; +} + +static String describeChannelMap (const std::vector& channelMap) +{ + String result ("["); + + for (std::size_t i = 0; i < channelMap.size(); ++i) + { + if (i != 0) + result << ", "; + + result << String (channelMap[i]); + } + + result << "]"; + return result; +} + +static String describeChannelBits (const BigInteger& channels) +{ + return "set=" + String (channels.countNumberOfSetBits()) + + ", highest=" + String (channels.getHighestBit()) + + ", bits=" + channels.toString (2); +} + +static String describeAudioDeviceID (AudioDeviceID deviceID) +{ + if (deviceID == kAudioObjectUnknown || deviceID == 0) + return "none"; + + auto result = getAudioDeviceName (deviceID); + + if (result.isEmpty()) + result = ""; + + result << " [" << String (deviceID) << "]"; + + if (const auto uid = getAudioDeviceUID (deviceID); uid.isNotEmpty()) + result << ", uid=" << uid; + + result << ", inputs=" << String (getNumChannelsForAudioDevice (deviceID, true)) + << ", outputs=" << String (getNumChannelsForAudioDevice (deviceID, false)); + + if (isAggregateAudioDevice (deviceID)) + result << ", aggregate"; + + return result; +} + +YUP_END_IGNORE_WARNINGS_GCC_LIKE + +#define YUP_SYSTEMAUDIOVOL_IMPLEMENTED 1 + +float YUP_CALLTYPE SystemAudioVolume::getGain() +{ + return SystemObject {}.getDefaultDevice (PlaybackDirection::output).getMainVolume(); +} + +bool YUP_CALLTYPE SystemAudioVolume::setGain (float gain) +{ + return SystemObject {}.getDefaultDevice (PlaybackDirection::output).setMainVolume (gain); +} + +bool YUP_CALLTYPE SystemAudioVolume::isMuted() +{ + return SystemObject {}.getDefaultDevice (PlaybackDirection::output).isMuted(); +} + +bool YUP_CALLTYPE SystemAudioVolume::setMuted (bool mute) +{ + return SystemObject {}.getDefaultDevice (PlaybackDirection::output).setMute (mute); +} + +//============================================================================== +template +static T findNearestValue (const Array& values, T target) +{ + if (values.isEmpty()) + return target; + + const auto it = std::lower_bound (values.begin(), values.end(), target); + + if (it == values.begin()) + return *it; + + if (it == values.end()) + return *(it - 1); + + const T upper = *it; + const T lower = *(it - 1); + return std::abs (target - lower) < std::abs (target - upper) ? lower : upper; +} + +template +static bool tryMultiple (Fn predicate, int maxTries) +{ + if (predicate()) + return true; + + for (int i = 1; i < maxTries; ++i) + { + Thread::yield(); + if (predicate()) + return true; + } + + return false; +} + +//============================================================================== +struct AggregateDeviceDescription +{ + struct SubDevice + { + AudioDeviceID deviceID = kAudioObjectUnknown; + String uid; + }; + + String name; + AudioDeviceID clockDeviceID = kAudioObjectUnknown; + std::vector subDevices; + std::array, 2> channelMaps; + + AggregateDeviceDescription (const String& nameIn, AudioDeviceID inputDeviceID, AudioDeviceID outputDeviceID) + : name (nameIn) + { + clockDeviceID = findClockingDeviceID (outputDeviceID); + + if (clockDeviceID == kAudioObjectUnknown) + clockDeviceID = findClockingDeviceID (inputDeviceID); + + addDevice (outputDeviceID, PlaybackDirection::output); + addDevice (inputDeviceID, PlaybackDirection::input); + } + + bool isEmpty() const noexcept { return subDevices.empty(); } + + AudioDeviceID createAggregateDevice() const + { + AudioDeviceID result = kAudioObjectUnknown; + const auto dict = toDictionary(); + const OSStatus status = AudioHardwareCreateAggregateDevice (dict.get(), &result); + return status == noErr ? result : kAudioObjectUnknown; + } + +private: + static AudioDeviceID findClockingDeviceID (AudioDeviceID deviceID) + { + if (deviceID == kAudioObjectUnknown || deviceID == 0) + return kAudioObjectUnknown; + + if (! isAggregateAudioDevice (deviceID)) + return deviceID; + + const AggregateAudioDevice aggregate (deviceID); + const auto uid = aggregate.getClockingDeviceUid(); + + if (uid.isNotEmpty()) + { + if (const auto translated = SystemObject {}.translateUidToDevice (uid); translated.isValid()) + return translated.getId(); + } + + const auto subs = getFlattenedAudioDeviceIDs (deviceID); + return subs.empty() ? kAudioObjectUnknown : subs.front(); + } + + int getFirstChannelIndex (AudioDeviceID deviceID, PlaybackDirection direction) const + { + int index = 0; + + for (const auto& sub : subDevices) + { + if (sub.deviceID == deviceID) + break; + + index += getDirectNumChannelsForDevice (sub.deviceID, direction == PlaybackDirection::input); + } + + return index; + } + + void addDevice (AudioDeviceID deviceID, PlaybackDirection direction) + { + if (deviceID == kAudioObjectUnknown || deviceID == 0) + return; + + for (const auto subDeviceID : getFlattenedAudioDeviceIDs (deviceID)) + { + const auto uid = getAudioDeviceUID (subDeviceID); + + if (std::none_of (subDevices.begin(), subDevices.end(), [subDeviceID] (const auto& s) + { + return s.deviceID == subDeviceID; + })) + subDevices.push_back ({ subDeviceID, uid }); + + const auto numChannels = getDirectNumChannelsForDevice (subDeviceID, direction == PlaybackDirection::input); + const auto baseChannel = getFirstChannelIndex (subDeviceID, direction); + auto& map = channelMaps[toDirectionIndex (direction)]; + + for (int ch = 0; ch < numChannels; ++ch) + map.push_back (baseChannel + ch); + } + } + + ScopedCFDictionary toDictionary() const + { + const auto clockUid = getAudioDeviceUID (clockDeviceID); + + static constexpr UInt32 kDriftCompensationMaxQuality = 0x7F; + + ScopedCFArray subDeviceArray; + + for (const auto& sub : subDevices) + { + ScopedCFDictionary subDict; + subDict.setString (kAudioSubDeviceUIDKey, sub.uid); + subDict.setInt (kAudioSubDeviceDriftCompensationKey, sub.deviceID != clockDeviceID ? 1 : 0); + subDict.setInt (kAudioSubDeviceDriftCompensationQualityKey, kDriftCompensationMaxQuality); + subDeviceArray.appendDictionary (subDict); + } + + constexpr auto mainSubDeviceKey = +#if defined(kAudioAggregateDeviceMainSubDeviceKey) + kAudioAggregateDeviceMainSubDeviceKey; +#else + kAudioAggregateDeviceMasterSubDeviceKey; +#endif + + ScopedCFDictionary description; + description.setString (kAudioAggregateDeviceUIDKey, Uuid().toString()); + description.setString (kAudioAggregateDeviceNameKey, name); + description.setInt (kAudioAggregateDeviceIsPrivateKey, 1); + description.setArray (kAudioAggregateDeviceSubDeviceListKey, subDeviceArray); + + if (clockUid.isNotEmpty()) + { + description.setString (kAudioAggregateDeviceClockDeviceKey, clockUid); + description.setString (mainSubDeviceKey, clockUid); + } + + return description; + } +}; + +//============================================================================== +struct CoreAudioClasses +{ + class CoreAudioIODeviceType; + class CoreAudioIODevice; + + //============================================================================== + class CoreAudioInternal final : private Timer + , private AsyncUpdater + { + auto err2log() const + { + return [this] (OSStatus err) + { + OK (err); + }; + } + + public: + CoreAudioInternal (CoreAudioIODevice& d, + AudioDeviceID id, + bool hasInput, + bool hasOutput, + AudioDeviceID inputChannelDeviceIDIn = kAudioObjectUnknown, + AudioDeviceID outputChannelDeviceIDIn = kAudioObjectUnknown, + std::array, 2> channelMapsIn = {}) + : owner (d) + , deviceID (id) + , channelDeviceIDs { inputChannelDeviceIDIn != kAudioObjectUnknown ? inputChannelDeviceIDIn : id, + outputChannelDeviceIDIn != kAudioObjectUnknown ? outputChannelDeviceIDIn : id } + , channelMaps (std::move (channelMapsIn)) + , inStream (hasInput ? new Stream (true, *this, {}) : nullptr) + , outStream (hasOutput ? new Stream (false, *this, {}) : nullptr) + { + jassert (deviceID != 0); + + updateDetailsFromDevice(); + YUP_MODULE_DBG (CORE_AUDIO, "Creating CoreAudioInternal\n" + << (inStream != nullptr ? (" inputDeviceId " + String (deviceID) + "\n") : "") << (outStream != nullptr ? (" outputDeviceId " + String (deviceID) + "\n") : "") << getDeviceDetails().joinIntoString ("\n ")); + + const PropertyAddress wildcardAddress { kAudioObjectPropertySelectorWildcard, kAudioObjectPropertyScopeWildcard, kAudioObjectPropertyElementWildcard }; + AudioObjectAddPropertyListener (deviceID, wildcardAddress.get(), deviceListenerProc, this); + } + + ~CoreAudioInternal() override + { + stopTimer(); + cancelPendingUpdate(); + + const PropertyAddress wildcardAddress { kAudioObjectPropertySelectorWildcard, kAudioObjectPropertyScopeWildcard, kAudioObjectPropertyElementWildcard }; + AudioObjectRemovePropertyListener (deviceID, wildcardAddress.get(), deviceListenerProc, this); + + stop (false); + } + + auto getStreams() const { return std::array { { inStream.get(), outStream.get() } }; } + + void allocateTempBuffers() + { + auto tempBufSize = bufferSize + 4; + + auto streams = getStreams(); + const auto total = std::accumulate (streams.begin(), streams.end(), 0, [] (int n, const auto& s) + { + return n + (s != nullptr ? s->channels : 0); + }); + audioBuffer.calloc (total * tempBufSize); + + auto channels = 0; + for (auto* stream : streams) + channels += stream != nullptr ? stream->allocateTempBuffers (tempBufSize, channels, audioBuffer) : 0; + } + + struct CallbackDetailsForChannel + { + int streamNum; + int dataOffsetSamples; + int dataStrideSamples; + }; + + Array getSampleRatesFromDevice() const + { + Array newSampleRates; + + if (auto ranges = audioObjectGetProperties (deviceID, + PropertyAddress (kAudioDevicePropertyAvailableNominalSampleRates, kAudioObjectPropertyScopeWildcard), + err2log()); + ! ranges.empty()) + { + for (const auto rate : SampleRateHelpers::getAllSampleRates()) + { + for (auto range = ranges.rbegin(); range != ranges.rend(); ++range) + { + if (range->mMinimum - 2 <= rate && rate <= range->mMaximum + 2) + { + newSampleRates.add (rate); + break; + } + } + } + } + + if (newSampleRates.isEmpty() && sampleRate > 0) + newSampleRates.add (sampleRate); + + auto nominalRate = getNominalSampleRate(); + + if ((nominalRate > 0) && ! newSampleRates.contains (nominalRate)) + newSampleRates.addUsingDefaultSort (nominalRate); + + return newSampleRates; + } + + Array getBufferSizesFromDevice() const + { + Array newBufferSizes; + + if (auto ranges = audioObjectGetProperties (deviceID, PropertyAddress (kAudioDevicePropertyBufferFrameSizeRange, kAudioObjectPropertyScopeWildcard), err2log()); ! ranges.empty()) + { + newBufferSizes.add ((int) (ranges[0].mMinimum + 15) & ~15); + + for (int i = 32; i <= 2048; i += 32) + { + for (auto range = ranges.rbegin(); range != ranges.rend(); ++range) + { + if (i >= range->mMinimum && i <= range->mMaximum) + { + newBufferSizes.addIfNotAlreadyThere (i); + break; + } + } + } + + if (bufferSize > 0) + newBufferSizes.addIfNotAlreadyThere (bufferSize); + } + + if (newBufferSizes.isEmpty() && bufferSize > 0) + newBufferSizes.add (bufferSize); + + return newBufferSizes; + } + + int getFrameSizeFromDevice() const + { + return static_cast (audioObjectGetProperty (deviceID, PropertyAddress (kAudioDevicePropertyBufferFrameSize, kAudioObjectPropertyScopeWildcard)).value_or (0)); + } + + bool isDeviceAlive() const + { + return deviceID != 0 + && audioObjectGetProperty (deviceID, PropertyAddress (kAudioDevicePropertyDeviceIsAlive, kAudioObjectPropertyScopeWildcard), err2log()).value_or (0) != 0; + } + + bool updateDetailsFromDevice (const BigInteger& activeIns, const BigInteger& activeOuts) + { + stopTimer(); + + if (! isDeviceAlive()) + return false; + + auto newSampleRate = getNominalSampleRate(); + auto newBufferSize = getFrameSizeFromDevice(); + + auto newBufferSizes = getBufferSizesFromDevice(); + auto newSampleRates = getSampleRatesFromDevice(); + + auto newInput = rawToUniquePtr (inStream != nullptr ? new Stream (true, *this, activeIns) : nullptr); + auto newOutput = rawToUniquePtr (outStream != nullptr ? new Stream (false, *this, activeOuts) : nullptr); + + auto newBitDepth = jmax (getBitDepth (newInput), getBitDepth (newOutput)); + +#if YUP_AUDIOWORKGROUP_TYPES_AVAILABLE + audioWorkgroup = [this]() -> AudioWorkgroup + { + if (auto* workgroupHandle = audioObjectGetProperty (deviceID, PropertyAddress (kAudioDevicePropertyIOThreadOSWorkgroup, kAudioObjectPropertyScopeWildcard)).value_or (nullptr)) + { + os_workgroup_t workgroup = (__bridge_transfer os_workgroup_t) workgroupHandle; + return makeRealAudioWorkgroup (workgroup); + } + + return {}; + }(); +#endif + + { + const ScopedLock sl (callbackLock); + + bitDepth = newBitDepth > 0 ? newBitDepth : 32; + + if (newSampleRate > 0) + sampleRate = newSampleRate; + + bufferSize = newBufferSize; + + sampleRates.swapWith (newSampleRates); + bufferSizes.swapWith (newBufferSizes); + + std::swap (inStream, newInput); + std::swap (outStream, newOutput); + + allocateTempBuffers(); + } + + YUP_MODULE_DBG (CORE_AUDIO, "Updated device details: deviceID=" << String (deviceID) << "\n " << getDeviceDetails().joinIntoString ("\n ")); + + return true; + } + + bool updateDetailsFromDevice() + { + return updateDetailsFromDevice (getActiveChannels (inStream), getActiveChannels (outStream)); + } + + StringArray getDeviceDetails() + { + StringArray result; + + String availableSampleRates ("Available sample rates:"); + + for (auto& s : sampleRates) + availableSampleRates << " " << s; + + result.add (availableSampleRates); + result.add ("Sample rate: " + String (sampleRate)); + String availableBufferSizes ("Available buffer sizes:"); + + for (auto& b : bufferSizes) + availableBufferSizes << " " << b; + + result.add (availableBufferSizes); + result.add ("Buffer size: " + String (bufferSize)); + result.add ("Bit depth: " + String (bitDepth)); + result.add ("Input latency: " + String (getLatency (inStream))); + result.add ("Output latency: " + String (getLatency (outStream))); + result.add ("Input channel names: " + getChannelNames (inStream)); + result.add ("Output channel names: " + getChannelNames (outStream)); + + return result; + } + + static auto getScope (bool input) + { + return getAudioDevicePropertyScope (input); + } + + AudioDeviceID getChannelDeviceID (bool input) const + { + return channelDeviceIDs[static_cast (getDirectionIndex (input))]; + } + + const std::vector& getChannelMap (bool input) const + { + return channelMaps[static_cast (getDirectionIndex (input))]; + } + + //============================================================================== + StringArray getSources (bool input) + { + StringArray s; + auto types = audioObjectGetProperties (deviceID, PropertyAddress (kAudioDevicePropertyDataSources, kAudioObjectPropertyScopeWildcard)); + + for (auto type : types) + { + AudioValueTranslation avt; + char buffer[256]; + + avt.mInputData = &type; + avt.mInputDataSize = sizeof (UInt32); + avt.mOutputData = buffer; + avt.mOutputDataSize = 256; + + UInt32 transSize = sizeof (avt); + const PropertyAddress pa { kAudioDevicePropertyDataSourceNameForID, getScope (input) }; + + if (OK (AudioObjectGetPropertyData (deviceID, pa.get(), 0, nullptr, &transSize, &avt))) + s.add (buffer); + } + + return s; + } + + int getCurrentSourceIndex (bool input) const + { + if (deviceID != 0) + { + if (auto currentSourceID = audioObjectGetProperty (deviceID, PropertyAddress (kAudioDevicePropertyDataSource, getScope (input)), err2log())) + { + auto types = audioObjectGetProperties (deviceID, PropertyAddress (kAudioDevicePropertyDataSources, kAudioObjectPropertyScopeWildcard)); + + if (auto it = std::find (types.begin(), types.end(), *currentSourceID); it != types.end()) + return static_cast (std::distance (types.begin(), it)); + } + } + + return -1; + } + + void setCurrentSourceIndex (int index, bool input) + { + if (deviceID != 0) + { + auto types = audioObjectGetProperties (deviceID, PropertyAddress (kAudioDevicePropertyDataSources, kAudioObjectPropertyScopeWildcard)); + + if (isPositiveAndBelow (index, static_cast (types.size()))) + { + audioObjectSetProperty (deviceID, PropertyAddress (kAudioDevicePropertyDataSource, getScope (input)), types[static_cast (index)], err2log()); + } + } + } + + double getNominalSampleRate() const + { + return static_cast (audioObjectGetProperty (deviceID, PropertyAddress (kAudioDevicePropertyNominalSampleRate), err2log()).value_or (0.0)); + } + + bool setNominalSampleRate (double newSampleRate) const + { + const auto currentSampleRate = getNominalSampleRate(); + + if (std::abs (currentSampleRate - newSampleRate) < 1.0) + { + YUP_MODULE_DBG (CORE_AUDIO, "Sample-rate already set: deviceID=" << String (deviceID) << ", sampleRate=" << String (currentSampleRate)); + return true; + } + + const auto result = audioObjectSetProperty (deviceID, PropertyAddress (kAudioDevicePropertyNominalSampleRate), static_cast (newSampleRate), err2log()); + + YUP_MODULE_DBG (CORE_AUDIO, "Set sample-rate " << (result ? "succeeded" : "failed") << ": deviceID=" << String (deviceID) << ", current=" << String (currentSampleRate) << ", requested=" << String (newSampleRate) << ", reported=" << String (getNominalSampleRate())); + + return result; + } + + //============================================================================== + String reopen (const BigInteger& ins, const BigInteger& outs, double newSampleRate, int bufferSizeSamples) + { + YUP_MODULE_DBG (CORE_AUDIO, "Reopen requested: deviceID=" << String (deviceID) << ", sampleRate=" << String (newSampleRate) << ", bufferSize=" << String (bufferSizeSamples) << ", inputs={" << describeChannelBits (ins) << "}" + << ", outputs={" << describeChannelBits (outs) << "}"); + + callbacksAllowed = false; + const ScopeGuard scope { [&] + { + callbacksAllowed = true; + } }; + + stopTimer(); + + stop (false); + + if (! setNominalSampleRate (newSampleRate)) + { + updateDetailsFromDevice (ins, outs); + YUP_MODULE_DBG (CORE_AUDIO, "Reopen failed: couldn't change sample-rate, deviceID=" << String (deviceID)); + return "Couldn't change sample rate"; + } + + if (! audioObjectSetProperty (deviceID, PropertyAddress (kAudioDevicePropertyBufferFrameSize), static_cast (bufferSizeSamples), err2log())) + { + updateDetailsFromDevice (ins, outs); + YUP_MODULE_DBG (CORE_AUDIO, "Reopen failed: couldn't change buffer size, deviceID=" << String (deviceID)); + return "Couldn't change buffer size"; + } + + updateDetailsFromDevice (ins, outs); + sampleRate = newSampleRate; + bufferSize = bufferSizeSamples; + + if (sampleRates.size() == 0) + { + YUP_MODULE_DBG (CORE_AUDIO, "Reopen failed: no sample-rates, deviceID=" << String (deviceID)); + return "Device has no available sample-rates"; + } + + if (bufferSizes.size() == 0) + { + YUP_MODULE_DBG (CORE_AUDIO, "Reopen failed: no buffer-sizes, deviceID=" << String (deviceID)); + return "Device has no available buffer-sizes"; + } + + YUP_MODULE_DBG (CORE_AUDIO, "Reopen completed: deviceID=" << String (deviceID) << ", sampleRate=" << String (sampleRate) << ", bufferSize=" << String (bufferSize) << ", inputChannels=" << String (getChannels (inStream)) << ", outputChannels=" << String (getChannels (outStream)) << ", bitDepth=" << String (bitDepth) << ", inputLatency=" << String (getLatency (inStream)) << ", outputLatency=" << String (getLatency (outStream))); + + return {}; + } + + bool start (AudioIODeviceCallback* callbackToNotify) + { + const ScopedLock sl (callbackLock); + + YUP_MODULE_DBG (CORE_AUDIO, "Start requested: deviceID=" << String (deviceID) << ", callback=" << (callbackToNotify != nullptr ? "set" : "null") << ", hasProc=" << (scopedProcID.get() != nullptr ? "true" : "false") << ", sampleRate=" << String (sampleRate) << ", bufferSize=" << String (bufferSize) << ", inputChannels=" << String (getChannels (inStream)) << ", outputChannels=" << String (getChannels (outStream))); + + if (callback == nullptr && callbackToNotify != nullptr) + { + callback = callbackToNotify; + callback->audioDeviceAboutToStart (&owner); + } + + for (auto* stream : getStreams()) + if (stream != nullptr) + stream->previousSampleTime = invalidSampleTime; + + owner.hadDiscontinuity = false; + + if (scopedProcID.get() == nullptr && deviceID != 0) + { + scopedProcID = [&self = *this, + &lock = callbackLock, + nextProcID = ScopedAudioDeviceIOProcID { *this, deviceID, audioIOProc }, + dID = deviceID]() mutable -> ScopedAudioDeviceIOProcID + { + // It *looks* like AudioDeviceStart may start the audio callback running, and then + // immediately lock an internal mutex. + // The same mutex is locked before calling the audioIOProc. + // If we get very unlucky, then we can end up with thread A taking the callbackLock + // and calling AudioDeviceStart, followed by thread B taking the CoreAudio lock + // and calling into audioIOProc while thread A then attempts to take the CoreAudio + // lock. Keep AudioDeviceStart outside callbackLock to preserve the lock order. + + if (auto* procID = nextProcID.get()) + { + const ScopedUnlock su (lock); + + if (self.OK (AudioDeviceStart (dID, procID))) + return std::move (nextProcID); + } + + return {}; + }(); + } + + playing = scopedProcID.get() != nullptr && callback != nullptr; + + YUP_MODULE_DBG (CORE_AUDIO, "Start " << (playing.load() ? "succeeded" : "failed") << ": deviceID=" << String (deviceID) << ", hasProc=" << (scopedProcID.get() != nullptr ? "true" : "false")); + + return scopedProcID.get() != nullptr; + } + + AudioIODeviceCallback* stop (bool leaveInterruptRunning) + { + const ScopedLock sl (callbackLock); + + YUP_MODULE_DBG (CORE_AUDIO, "Stop requested: deviceID=" << String (deviceID) << ", leaveInterruptRunning=" << (leaveInterruptRunning ? "true" : "false") << ", hasCallback=" << (callback != nullptr ? "true" : "false") << ", hasProc=" << (scopedProcID.get() != nullptr ? "true" : "false") << ", playing=" << (playing.load() ? "true" : "false")); + + auto result = std::exchange (callback, nullptr); + + if (scopedProcID.get() != nullptr && (deviceID != 0) && ! leaveInterruptRunning) + { + audioDeviceStopPending = true; + + { + const ScopedUnlock ul (callbackLock); + + for (int i = 40; --i >= 0;) + { + if (audioDeviceStopPending == false) + break; + + Thread::sleep (50); + } + } + + scopedProcID = {}; + playing = false; + } + + YUP_MODULE_DBG (CORE_AUDIO, "Stop completed: deviceID=" << String (deviceID) << ", callbackReturned=" << (result != nullptr ? "true" : "false") << ", hasProc=" << (scopedProcID.get() != nullptr ? "true" : "false") << ", playing=" << (playing.load() ? "true" : "false") << ", stopPending=" << (audioDeviceStopPending ? "true" : "false")); + + return result; + } + + double getSampleRate() const { return sampleRate; } + + int getBufferSize() const { return bufferSize; } + + void audioCallback (const AudioTimeStamp* inputTimestamp, + const AudioTimeStamp* outputTimestamp, + const AudioBufferList* inInputData, + AudioBufferList* outOutputData) + { + const ScopedTryLock sl (callbackLock); + if (! sl.isLocked()) + { + clearOutputBuffers (outOutputData); + return; + } + + if (audioDeviceStopPending) + { + if (OK (AudioDeviceStop (deviceID, scopedProcID.get()), false)) + audioDeviceStopPending = false; + + return; + } + + const auto numInputChans = getChannels (inStream); + const auto numOutputChans = getChannels (outStream); + + if (callback != nullptr) + { + for (int i = numInputChans; --i >= 0;) + { + auto& info = inStream->channelInfo.getReference (i); + auto dest = inStream->tempBuffers[i]; + auto src = ((const float*) inInputData->mBuffers[info.streamNum].mData) + info.dataOffsetSamples; + auto stride = info.dataStrideSamples; + + if (stride != 0) // if this is zero, info is invalid + { + for (int j = bufferSize; --j >= 0;) + { + *dest++ = *src; + src += stride; + } + } + } + + for (auto* stream : getStreams()) + if (stream != nullptr) + owner.hadDiscontinuity |= stream->checkTimestampsForDiscontinuity (stream == inStream.get() ? inputTimestamp + : outputTimestamp); + + const auto* timeStamp = numOutputChans > 0 ? outputTimestamp : inputTimestamp; + const auto nanos = timeStamp != nullptr ? timeConversions.hostTimeToNanos (timeStamp->mHostTime) : 0; + const AudioIODeviceCallbackContext context { + timeStamp != nullptr ? &nanos : nullptr, + }; + + callback->audioDeviceIOCallbackWithContext (getTempBuffers (inStream), numInputChans, getTempBuffers (outStream), numOutputChans, bufferSize, context); + + if (! getChannelMap (false).empty()) + clearOutputBuffers (outOutputData); + + for (int i = numOutputChans; --i >= 0;) + { + auto& info = outStream->channelInfo.getReference (i); + auto src = outStream->tempBuffers[i]; + auto dest = ((float*) outOutputData->mBuffers[info.streamNum].mData) + info.dataOffsetSamples; + auto stride = info.dataStrideSamples; + + if (stride != 0) // if this is zero, info is invalid + { + for (int j = bufferSize; --j >= 0;) + { + *dest = *src++; + dest += stride; + } + } + } + } + else + { + clearOutputBuffers (outOutputData); + } + + for (auto* stream : getStreams()) + if (stream != nullptr) + stream->previousSampleTime += static_cast (bufferSize); + } + + // called by callbacks (possibly off the main thread) + void deviceDetailsChanged() + { + if (callbacksAllowed.get() == 1) + startTimer (100); + } + + // called by callbacks (possibly off the main thread) + void deviceRequestedRestart() + { + owner.restart(); + triggerAsyncUpdate(); + } + + bool isPlaying() const { return playing.load(); } + + //============================================================================== + struct Stream + { + Stream (bool isInput, CoreAudioInternal& parent, const BigInteger& activeRequested) + : input (isInput) + , latency (getLatencyFromDevice (isInput, parent)) + , bitDepth (getBitDepthFromDevice (isInput, parent)) + , chanNames (getChannelNames (isInput, parent)) + , activeChans ([&activeRequested, clearFrom = chanNames.size()] + { + auto result = activeRequested; + result.setRange (clearFrom, result.getHighestBit() + 1 - clearFrom, false); + return result; + }()) + , channelInfo (getChannelInfos (isInput, parent, activeChans)) + , channels (static_cast (channelInfo.size())) + { + } + + int allocateTempBuffers (int tempBufSize, int channelCount, HeapBlock& buffer) + { + tempBuffers.calloc (channels + 2); + + for (int i = 0; i < channels; ++i) + tempBuffers[i] = buffer + channelCount++ * tempBufSize; + + return channels; + } + + template + static auto visitChannels (bool isInput, CoreAudioInternal& parent, Visitor&& visitor) + { + struct Args + { + int stream, channelIdx, chanNum, streamChannels; + }; + + using VisitorResultType = typename std::invoke_result_t::value_type; + Array result; + std::vector physicalChannels; + + if (auto bufList = audioObjectGetProperty (parent.deviceID, PropertyAddress (kAudioDevicePropertyStreamConfiguration, getScope (isInput)), parent.err2log())) + { + const int numStreams = static_cast (bufList->mNumberBuffers); + int chanNum = 0; + + for (int i = 0; i < numStreams; ++i) + { + auto& b = bufList->mBuffers[i]; + + for (unsigned int j = 0; j < b.mNumberChannels; ++j) + physicalChannels.push_back (Args { i, static_cast (j), chanNum++, static_cast (b.mNumberChannels) }); + } + } + + const auto addResult = [&] (const Args& args) + { + // Passing an anonymous struct ensures that callback can't confuse the argument order + if (auto opt = visitor (args)) + result.add (std::move (*opt)); + }; + + const auto& channelMap = parent.getChannelMap (isInput); + + if (channelMap.empty()) + { + for (const auto& args : physicalChannels) + addResult (args); + } + else + { + auto logicalChannel = 0; + + for (const auto physicalChannel : channelMap) + { + if (physicalChannel >= 0 && physicalChannel < static_cast (physicalChannels.size())) + { + auto args = physicalChannels[static_cast (physicalChannel)]; + args.chanNum = logicalChannel; + addResult (args); + } + + ++logicalChannel; + } + } + + return result; + } + + static Array getChannelInfos (bool isInput, CoreAudioInternal& parent, const BigInteger& active) + { + return visitChannels (isInput, parent, [&] (const auto& args) -> std::optional + { + if (! active[args.chanNum]) + return {}; + + return CallbackDetailsForChannel { args.stream, args.channelIdx, args.streamChannels }; + }); + } + + static StringArray getChannelNames (bool isInput, CoreAudioInternal& parent) + { + const auto channelDeviceID = parent.getChannelDeviceID (isInput); + auto subDeviceGroups = getAggregateSubDeviceChannelGroups (channelDeviceID, isInput); + std::size_t subDeviceGroupIndex = 0; + + const auto getSubDeviceName = [&]() -> String + { + while (subDeviceGroupIndex < subDeviceGroups.size() && subDeviceGroups[subDeviceGroupIndex].remainingChannels == 0) + ++subDeviceGroupIndex; + + if (subDeviceGroupIndex >= subDeviceGroups.size()) + return {}; + + --subDeviceGroups[subDeviceGroupIndex].remainingChannels; + return subDeviceGroups[subDeviceGroupIndex].name; + }; + + auto names = visitChannels (isInput, parent, [&] (const auto& args) -> std::optional + { + const auto element = static_cast (args.chanNum + 1); + + String name = audioObjectGetStringProperty (channelDeviceID, PropertyAddress (kAudioObjectPropertyElementName, getScope (isInput), element)); + + if (name.isEmpty()) + name << (isInput ? "Input " : "Output ") << (args.chanNum + 1); + + if (auto subDeviceName = getSubDeviceName(); subDeviceName.isNotEmpty()) + name << " (" << subDeviceName << ")"; + + return name; + }); + + return { names }; + } + + static int getBitDepthFromDevice (bool isInput, CoreAudioInternal& parent) + { + return static_cast (audioObjectGetProperty (parent.deviceID, PropertyAddress (kAudioStreamPropertyPhysicalFormat, getScope (isInput)), parent.err2log()) + .value_or (AudioStreamBasicDescription {}) + .mBitsPerChannel); + } + + static int getLatencyFromDevice (bool isInput, CoreAudioInternal& parent) + { + const auto scope = getScope (isInput); + + const auto deviceLatency = audioObjectGetProperty (parent.deviceID, PropertyAddress (kAudioDevicePropertyLatency, scope)).value_or (0); + + const auto safetyOffset = audioObjectGetProperty (parent.deviceID, PropertyAddress (kAudioDevicePropertySafetyOffset, scope)).value_or (0); + + const auto framesInBuffer = audioObjectGetProperty (parent.deviceID, PropertyAddress (kAudioDevicePropertyBufferFrameSize, kAudioObjectPropertyScopeWildcard)).value_or (0); + + UInt32 streamLatency = 0; + + if (auto streams = audioObjectGetProperties (parent.deviceID, PropertyAddress (kAudioDevicePropertyStreams, scope)); ! streams.empty()) + streamLatency = audioObjectGetProperty (streams.front(), PropertyAddress (kAudioStreamPropertyLatency, scope)).value_or (0); + + return static_cast (deviceLatency + safetyOffset + framesInBuffer + streamLatency); + } + + bool checkTimestampsForDiscontinuity (const AudioTimeStamp* timestamp) noexcept + { + if (channels > 0) + { + jassert (timestamp == nullptr || (((timestamp->mFlags & kAudioTimeStampSampleTimeValid) != 0) && ((timestamp->mFlags & kAudioTimeStampHostTimeValid) != 0))); + + if (exactlyEqual (previousSampleTime, invalidSampleTime)) + previousSampleTime = timestamp != nullptr ? timestamp->mSampleTime : 0.0; + + if (timestamp != nullptr && std::fabs (previousSampleTime - timestamp->mSampleTime) >= 1.0) + { + previousSampleTime = timestamp->mSampleTime; + return true; + } + } + + return false; + } + + //============================================================================== + const bool input; + const int latency; + const int bitDepth; + const StringArray chanNames; + const BigInteger activeChans; + const Array channelInfo; + const int channels = 0; + Float64 previousSampleTime; + + HeapBlock tempBuffers; + + YUP_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (Stream) + }; + + template + static auto getWithDefault (const std::unique_ptr& ptr, Callback&& callback) + { + return ptr != nullptr ? callback (*ptr) : decltype (callback (*ptr)) {}; + } + + template + static auto getWithDefault (const std::unique_ptr& ptr, Value (Stream::* member)) + { + return getWithDefault (ptr, [&] (Stream& s) + { + return s.*member; + }); + } + + static int getLatency (const std::unique_ptr& ptr) { return getWithDefault (ptr, &Stream::latency); } + + static int getBitDepth (const std::unique_ptr& ptr) { return getWithDefault (ptr, &Stream::bitDepth); } + + static int getChannels (const std::unique_ptr& ptr) { return getWithDefault (ptr, &Stream::channels); } + + static int getNumChannelNames (const std::unique_ptr& ptr) { return getWithDefault (ptr, &Stream::chanNames).size(); } + + static String getChannelNames (const std::unique_ptr& ptr) { return getWithDefault (ptr, &Stream::chanNames).joinIntoString (" "); } + + static BigInteger getActiveChannels (const std::unique_ptr& ptr) { return getWithDefault (ptr, &Stream::activeChans); } + + static float** getTempBuffers (const std::unique_ptr& ptr) + { + return getWithDefault (ptr, [] (auto& s) + { + return s.tempBuffers.get(); + }); + } + + //============================================================================== + static constexpr Float64 invalidSampleTime = std::numeric_limits::max(); + + CoreAudioIODevice& owner; + int bitDepth = 32; + std::atomic xruns { 0 }; + Array sampleRates; + Array bufferSizes; + AudioDeviceID deviceID; + std::array channelDeviceIDs; + std::array, 2> channelMaps; + std::unique_ptr inStream, outStream; + + AudioWorkgroup audioWorkgroup; + + private: + class ScopedAudioDeviceIOProcID + { + public: + ScopedAudioDeviceIOProcID() = default; + + ScopedAudioDeviceIOProcID (CoreAudioInternal& coreAudio, AudioDeviceID d, AudioDeviceIOProc audioIOProc) + : deviceID (d) + { + if (! coreAudio.OK (AudioDeviceCreateIOProcID (deviceID, audioIOProc, &coreAudio, &proc))) + proc = {}; + } + + ~ScopedAudioDeviceIOProcID() noexcept + { + if (proc != AudioDeviceIOProcID {}) + AudioDeviceDestroyIOProcID (deviceID, proc); + } + + ScopedAudioDeviceIOProcID (ScopedAudioDeviceIOProcID&& other) noexcept + { + swap (other); + } + + ScopedAudioDeviceIOProcID& operator= (ScopedAudioDeviceIOProcID&& other) noexcept + { + ScopedAudioDeviceIOProcID { std::move (other) }.swap (*this); + return *this; + } + + AudioDeviceIOProcID get() const { return proc; } + + private: + void swap (ScopedAudioDeviceIOProcID& other) noexcept + { + std::swap (other.deviceID, deviceID); + std::swap (other.proc, proc); + } + + AudioDeviceID deviceID = {}; + AudioDeviceIOProcID proc = {}; + }; + + //============================================================================== + ScopedAudioDeviceIOProcID scopedProcID; + CoreAudioTimeConversions timeConversions; + AudioIODeviceCallback* callback = nullptr; + CriticalSection callbackLock; + std::atomic audioDeviceStopPending { false }; + std::atomic playing { false }; + double sampleRate = 0; + int bufferSize = 0; + HeapBlock audioBuffer; + Atomic callbacksAllowed { 1 }; + + //============================================================================== + static void clearOutputBuffers (AudioBufferList* outputData) noexcept + { + if (outputData == nullptr) + return; + + for (UInt32 i = 0; i < outputData->mNumberBuffers; ++i) + zeromem (outputData->mBuffers[i].mData, + outputData->mBuffers[i].mDataByteSize); + } + + //============================================================================== + void timerCallback() override + { + stopTimer(); + auto oldSampleRate = sampleRate; + auto oldBufferSize = bufferSize; + + YUP_MODULE_DBG (CORE_AUDIO, "Device change timer: deviceID=" << String (deviceID) << ", oldSampleRate=" << String (oldSampleRate) << ", oldBufferSize=" << String (oldBufferSize)); + + if (! updateDetailsFromDevice()) + { + YUP_MODULE_DBG (CORE_AUDIO, "Device change update failed: deviceID=" << String (deviceID)); + owner.stopInternal(); + } + else if ((oldBufferSize != bufferSize || ! approximatelyEqual (oldSampleRate, sampleRate)) && owner.shouldRestartDevice()) + { + YUP_MODULE_DBG (CORE_AUDIO, "Device change requires restart: deviceID=" << String (deviceID) << ", newSampleRate=" << String (sampleRate) << ", newBufferSize=" << String (bufferSize)); + owner.restart(); + } + else + { + YUP_MODULE_DBG (CORE_AUDIO, "Device change applied without restart: deviceID=" << String (deviceID) << ", newSampleRate=" << String (sampleRate) << ", newBufferSize=" << String (bufferSize)); + } + } + + void handleAsyncUpdate() override + { + if (owner.deviceType != nullptr) + owner.deviceType->audioDeviceListChanged(); + } + + static OSStatus audioIOProc (AudioDeviceID /*inDevice*/, + [[maybe_unused]] const AudioTimeStamp* inNow, + const AudioBufferList* inInputData, + const AudioTimeStamp* inInputTime, + AudioBufferList* outOutputData, + const AudioTimeStamp* inOutputTime, + void* device) + { + static_cast (device)->audioCallback (inInputTime, inOutputTime, inInputData, outOutputData); + return noErr; + } + + static OSStatus deviceListenerProc (AudioDeviceID /*inDevice*/, + UInt32 numAddresses, + const AudioObjectPropertyAddress* pa, + void* inClientData) + { + auto& intern = *static_cast (inClientData); + + const auto xruns = (int) std::count_if (pa, pa + numAddresses, [] (const AudioObjectPropertyAddress& x) + { + return x.mSelector == kAudioDeviceProcessorOverload; + }); + + intern.xruns.fetch_add (xruns); + + const auto detailsChanged = std::any_of (pa, pa + numAddresses, [] (const AudioObjectPropertyAddress& x) + { + constexpr UInt32 selectors[] { + kAudioDevicePropertyBufferSize, + kAudioDevicePropertyBufferFrameSize, + kAudioDevicePropertyNominalSampleRate, + kAudioDevicePropertyStreamFormat, + kAudioDevicePropertyDeviceIsAlive, + kAudioStreamPropertyPhysicalFormat, + }; + + return std::find (std::begin (selectors), std::end (selectors), x.mSelector) != std::end (selectors); + }); + + const auto requestedRestart = std::any_of (pa, pa + numAddresses, [] (const AudioObjectPropertyAddress& x) + { + constexpr UInt32 selectors[] { + kAudioDevicePropertyDeviceHasChanged, + kAudioObjectPropertyOwnedObjects, + }; + + return std::find (std::begin (selectors), std::end (selectors), x.mSelector) != std::end (selectors); + }); + + if (detailsChanged) + intern.deviceDetailsChanged(); + + if (requestedRestart) + intern.deviceRequestedRestart(); + + return noErr; + } + + //============================================================================== + bool OK (const OSStatus errorCode, bool logError = true) const + { + if (errorCode == noErr) + return true; + + const String errorMessage ("CoreAudio error: " + String::toHexString ((int) errorCode)); + + if (logError) + YUP_MODULE_DBG (CORE_AUDIO, errorMessage); + + if (callback != nullptr) + callback->audioDeviceError (errorMessage); + + return false; + } + + YUP_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (CoreAudioInternal) + }; + + //============================================================================== + class CoreAudioIODevice final : public AudioIODevice + , private Timer + { + public: + CoreAudioIODevice (CoreAudioIODeviceType* dt, + const String& deviceName, + AudioDeviceID inputDeviceId, + AudioDeviceID outputDeviceId, + int inputDeviceIndexIn, + int outputDeviceIndexIn) + : AudioIODevice (deviceName, "CoreAudio") + , deviceType (dt) + , inputDeviceIndex (inputDeviceIndexIn) + , outputDeviceIndex (outputDeviceIndexIn) + { + internal = [this, &deviceName, &inputDeviceId, &outputDeviceId]() -> std::unique_ptr + { + if (outputDeviceId == 0 || outputDeviceId == inputDeviceId) + { + jassert (inputDeviceId != 0); + return std::make_unique (*this, + inputDeviceId, + true, + outputDeviceId != 0, + inputDeviceId, + outputDeviceId != 0 ? outputDeviceId : kAudioObjectUnknown); + } + + if (inputDeviceId == 0) + { + return std::make_unique (*this, + outputDeviceId, + false, + true, + kAudioObjectUnknown, + outputDeviceId); + } + + const AggregateDeviceDescription aggregateDesc (createPrivateAggregateDeviceName (deviceName), + inputDeviceId, + outputDeviceId); + + if (aggregateDesc.isEmpty()) + return nullptr; + + YUP_MODULE_DBG (CORE_AUDIO, "Creating private aggregate for input=" << getAudioDeviceName (inputDeviceId) << " [" << String (inputDeviceId) << "]" + << ", output=" << getAudioDeviceName (outputDeviceId) << " [" << String (outputDeviceId) << "]" + << ", subdevices=" << String (static_cast (aggregateDesc.subDevices.size())) << "\n inputMap=" << describeChannelMap (aggregateDesc.channelMaps[0]) << "\n outputMap=" << describeChannelMap (aggregateDesc.channelMaps[1]) << ", inputChannels=" << String (static_cast (aggregateDesc.channelMaps[0].size())) << ", outputChannels=" << String (static_cast (aggregateDesc.channelMaps[1].size())) << ", clockDevice=" << String (aggregateDesc.clockDeviceID)); + + aggregateDeviceID = aggregateDesc.createAggregateDevice(); + + YUP_MODULE_DBG (CORE_AUDIO, "Private aggregate creation " << (aggregateDeviceID != kAudioObjectUnknown ? "succeeded" : "failed") << ", deviceID=" << String (aggregateDeviceID)); + + if (aggregateDeviceID == kAudioObjectUnknown) + return nullptr; + + auto channelMaps = aggregateDesc.channelMaps; + return std::make_unique (*this, + aggregateDeviceID, + true, + true, + inputDeviceId, + outputDeviceId, + std::move (channelMaps)); + }(); + + if (internal == nullptr) + return; + + const PropertyAddress wildcardAddress { kAudioObjectPropertySelectorWildcard, kAudioObjectPropertyScopeWildcard, kAudioObjectPropertyElementWildcard }; + AudioObjectAddPropertyListener (kAudioObjectSystemObject, wildcardAddress.get(), hardwareListenerProc, internal.get()); + } + + ~CoreAudioIODevice() override + { + if (internal != nullptr) + { + close(); + + const PropertyAddress wildcardAddress { kAudioObjectPropertySelectorWildcard, kAudioObjectPropertyScopeWildcard, kAudioObjectPropertyElementWildcard }; + AudioObjectRemovePropertyListener (kAudioObjectSystemObject, wildcardAddress.get(), hardwareListenerProc, internal.get()); + } + + if (aggregateDeviceID != 0) + { + YUP_MODULE_DBG (CORE_AUDIO, "Destroying private aggregate: deviceID=" << String (aggregateDeviceID)); + AudioHardwareDestroyAggregateDevice (aggregateDeviceID); + } + } + + StringArray getOutputChannelNames() override { return internal->outStream != nullptr ? internal->outStream->chanNames : StringArray(); } + + StringArray getInputChannelNames() override { return internal->inStream != nullptr ? internal->inStream->chanNames : StringArray(); } + + bool isOpen() override { return isOpen_; } + + Array getAvailableSampleRates() override { return internal->sampleRates; } + + Array getAvailableBufferSizes() override { return internal->bufferSizes; } + + double getCurrentSampleRate() override { return internal->getSampleRate(); } + + int getCurrentBitDepth() override { return internal->bitDepth; } + + int getCurrentBufferSizeSamples() override { return internal->getBufferSize(); } + + int getXRunCount() const noexcept override { return internal->xruns.load (std::memory_order_relaxed); } + + bool isValid() const noexcept { return internal != nullptr; } + + int getIndexOfDevice (bool asInput) const + { + return asInput ? inputDeviceIndex + : outputDeviceIndex; + } + + int getDefaultBufferSize() override + { + int best = 0; + + for (int i = 0; best < 512 && i < internal->bufferSizes.size(); ++i) + best = internal->bufferSizes.getUnchecked (i); + + if (best == 0) + best = 512; + + return best; + } + + String open (const BigInteger& inputChannels, + const BigInteger& outputChannels, + double sampleRate, + int bufferSizeSamples) override + { + YUP_MODULE_DBG (CORE_AUDIO, "Open requested: " << getName() << ", inputChannels={" << describeChannelBits (inputChannels) << "}" + << ", outputChannels={" << describeChannelBits (outputChannels) << "}" + << ", requestedSampleRate=" << String (sampleRate) << ", requestedBufferSize=" << String (bufferSizeSamples)); + + isOpen_ = true; + internal->xruns.store (0); + + inputChannelsRequested = inputChannels; + outputChannelsRequested = outputChannels; + + if (bufferSizeSamples <= 0) + bufferSizeSamples = getDefaultBufferSize(); + + if (sampleRate <= 0) + sampleRate = internal->getNominalSampleRate(); + + lastError = internal->reopen (inputChannels, outputChannels, sampleRate, bufferSizeSamples); + + isOpen_ = lastError.isEmpty(); + + YUP_MODULE_DBG (CORE_AUDIO, "Open " << (isOpen_ ? "succeeded" : "failed") << ": " << getName() << ", sampleRate=" << String (getCurrentSampleRate()) << ", bufferSize=" << String (getCurrentBufferSizeSamples()) << ", activeInputs={" << describeChannelBits (getActiveInputChannels()) << "}" + << ", activeOutputs={" << describeChannelBits (getActiveOutputChannels()) << "}" << (lastError.isNotEmpty() ? ", error=" + lastError : String())); + + return lastError; + } + + void close() override + { + if (internal == nullptr) + return; + + YUP_MODULE_DBG (CORE_AUDIO, "Close requested: " << getName() << ", isOpen=" << (isOpen_ ? "true" : "false") << ", isPlaying=" << (internal->isPlaying() ? "true" : "false")); + + isOpen_ = false; + internal->stop (false); + } + + BigInteger getActiveOutputChannels() const override { return CoreAudioInternal::getActiveChannels (internal->outStream); } + + BigInteger getActiveInputChannels() const override { return CoreAudioInternal::getActiveChannels (internal->inStream); } + + int getOutputLatencyInSamples() override { return CoreAudioInternal::getLatency (internal->outStream); } + + int getInputLatencyInSamples() override { return CoreAudioInternal::getLatency (internal->inStream); } + + void start (AudioIODeviceCallback* callback) override + { + YUP_MODULE_DBG (CORE_AUDIO, "AudioIODevice start requested: " << getName() << ", callback=" << (callback != nullptr ? "set" : "null")); + + if (internal->start (callback)) + previousCallback = callback; + + YUP_MODULE_DBG (CORE_AUDIO, "AudioIODevice start completed: " << getName() << ", isPlaying=" << (internal->isPlaying() ? "true" : "false")); + } + + void stop() override + { + YUP_MODULE_DBG (CORE_AUDIO, "AudioIODevice stop requested: " << getName()); + + restartDevice = false; + stopAndGetLastCallback(); + } + + AudioIODeviceCallback* stopAndGetLastCallback() const + { + auto* lastCallback = internal->stop (true); + + if (lastCallback != nullptr) + lastCallback->audioDeviceStopped(); + + return lastCallback; + } + + AudioIODeviceCallback* stopInternal() + { + restartDevice = true; + return stopAndGetLastCallback(); + } + + AudioWorkgroup getWorkgroup() const override + { + return internal->audioWorkgroup; + } + + bool isPlaying() override + { + return internal->isPlaying(); + } + + String getLastError() override + { + return lastError; + } + + void audioDeviceListChanged() + { + if (deviceType != nullptr) + deviceType->audioDeviceListChanged(); + } + + // called by callbacks (possibly off the main thread) + void restart() + { + YUP_MODULE_DBG (CORE_AUDIO, "Restart requested: " << getName() << ", previousCallback=" << (previousCallback != nullptr ? "set" : "null")); + + { + const ScopedLock sl (closeLock); + previousCallback = stopInternal(); + } + + startTimer (100); + } + + bool setCurrentSampleRate (double newSampleRate) + { + const auto result = internal->setNominalSampleRate (newSampleRate); + + YUP_MODULE_DBG (CORE_AUDIO, "setCurrentSampleRate " << (result ? "succeeded" : "failed") << ": " << getName() << ", requested=" << String (newSampleRate) << ", current=" << String (getCurrentSampleRate())); + + return result; + } + + bool shouldRestartDevice() const noexcept { return restartDevice; } + + WeakReference deviceType; + bool hadDiscontinuity; + + private: + std::unique_ptr internal; + AudioDeviceID aggregateDeviceID = 0; + int inputDeviceIndex = -1, outputDeviceIndex = -1; + bool isOpen_ = false, restartDevice = true; + String lastError; + AudioIODeviceCallback* previousCallback = nullptr; + BigInteger inputChannelsRequested, outputChannelsRequested; + CriticalSection closeLock; + + void timerCallback() override + { + stopTimer(); + + YUP_MODULE_DBG (CORE_AUDIO, "Restart timer fired: " << getName() << ", sampleRate=" << String (getCurrentSampleRate()) << ", bufferSize=" << String (getCurrentBufferSizeSamples()) << ", previousCallback=" << (previousCallback != nullptr ? "set" : "null")); + + stopInternal(); + + internal->updateDetailsFromDevice(); + + open (inputChannelsRequested, outputChannelsRequested, getCurrentSampleRate(), getCurrentBufferSizeSamples()); + start (previousCallback); + } + + static OSStatus hardwareListenerProc (AudioDeviceID /*inDevice*/, + UInt32 numAddresses, + const AudioObjectPropertyAddress* pa, + void* inClientData) + { + const auto detailsChanged = std::any_of (pa, pa + numAddresses, [] (const AudioObjectPropertyAddress& x) + { + return x.mSelector == kAudioHardwarePropertyDevices; + }); + + if (detailsChanged) + static_cast (inClientData)->deviceDetailsChanged(); + + return noErr; + } + + YUP_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (CoreAudioIODevice) + }; + + //============================================================================== + class CoreAudioIODeviceType final : public AudioIODeviceType + , private AsyncUpdater + { + public: + CoreAudioIODeviceType() + : AudioIODeviceType ("CoreAudio") + { + // Remove stale private aggregate devices left behind by previous crashes. + // The private aggregate name encodes the PID of the creating process. + for (const auto& device : SystemObject {}.getAudioDevices()) + { + if (! device.isAggregateDevice()) + continue; + + const auto name = device.getName(); + + if (! isPrivateAggregateDeviceName (name)) + continue; + + const auto pid = getPrivateAggregateDeviceProcessID (name); + if (pid <= 0) + continue; + + const auto processExists = pid > 0 && (::kill (pid, 0) == 0 || errno != ESRCH); + + if (! processExists) + { + YUP_MODULE_DBG (CORE_AUDIO, "Destroying stale private aggregate: " << name); + AudioHardwareDestroyAggregateDevice (device.getId()); + } + } + + const PropertyAddress devicesAddress { kAudioHardwarePropertyDevices, kAudioObjectPropertyScopeWildcard, kAudioObjectPropertyElementWildcard }; + AudioObjectAddPropertyListener (kAudioObjectSystemObject, devicesAddress.get(), hardwareListenerProc, this); + } + + ~CoreAudioIODeviceType() override + { + cancelPendingUpdate(); + + const PropertyAddress devicesAddress { kAudioHardwarePropertyDevices, kAudioObjectPropertyScopeWildcard, kAudioObjectPropertyElementWildcard }; + AudioObjectRemovePropertyListener (kAudioObjectSystemObject, devicesAddress.get(), hardwareListenerProc, this); + } + + //============================================================================== + void scanForDevices() override + { + hasScanned = true; + + inputDeviceNames.clear(); + outputDeviceNames.clear(); + inputIds.clear(); + outputIds.clear(); + + struct DeviceEntry + { + String name; + String uid; + AudioDeviceID deviceID = kAudioObjectUnknown; + }; + + std::vector inputDevices, outputDevices; + + auto audioDevices = audioObjectGetProperties (kAudioObjectSystemObject, PropertyAddress (kAudioHardwarePropertyDevices, kAudioObjectPropertyScopeWildcard)); + + for (const auto audioDevice : audioDevices) + { + if (const auto nameString = audioObjectGetStringProperty (audioDevice, PropertyAddress (kAudioDevicePropertyDeviceNameCFString, kAudioObjectPropertyScopeWildcard)); nameString.isNotEmpty()) + { + if (isAggregateAudioDevice (audioDevice) && isPrivateAggregateDeviceName (nameString)) + { + YUP_MODULE_DBG (CORE_AUDIO, "Skipping private aggregate during scan: " << nameString << " [" << String (audioDevice) << "]"); + continue; + } + + const auto uidString = getAudioDeviceUID (audioDevice); + const auto numIns = getNumChannels (audioDevice, true); + const auto numOuts = getNumChannels (audioDevice, false); + + YUP_MODULE_DBG (CORE_AUDIO, "Found device: " << nameString << " [" << String (audioDevice) << "]" + << ", uid=" << uidString << ", inputs=" << String (numIns) << ", outputs=" << String (numOuts)); + + if (numIns > 0) + inputDevices.push_back ({ nameString, uidString, audioDevice }); + + if (numOuts > 0) + outputDevices.push_back ({ nameString, uidString, audioDevice }); + } + } + + const auto sortDevices = [] (std::vector& devices) + { + std::sort (devices.begin(), devices.end(), [] (const auto& a, const auto& b) + { + if (const auto byName = a.name.compareNatural (b.name); byName != 0) + return byName < 0; + + if (const auto byUid = a.uid.compare (b.uid); byUid != 0) + return byUid < 0; + + return a.deviceID < b.deviceID; + }); + }; + + const auto populateDeviceList = [] (const std::vector& devices, StringArray& names, Array& ids) + { + for (const auto& device : devices) + { + names.add (device.name); + ids.add (device.deviceID); + } + }; + + sortDevices (inputDevices); + sortDevices (outputDevices); + + populateDeviceList (inputDevices, inputDeviceNames, inputIds); + populateDeviceList (outputDevices, outputDeviceNames, outputIds); + + YUP_MODULE_DBG (CORE_AUDIO, "Scan complete: inputs=" << String (inputDeviceNames.size()) << ", outputs=" << String (outputDeviceNames.size())); + + inputDeviceNames.appendNumbersToDuplicates (false, true); + outputDeviceNames.appendNumbersToDuplicates (false, true); + } + + StringArray getDeviceNames (bool wantInputNames) const override + { + jassert (hasScanned); // need to call scanForDevices() before doing this + + return wantInputNames ? inputDeviceNames + : outputDeviceNames; + } + + int getDefaultDeviceIndex (bool forInput) const override + { + jassert (hasScanned); // need to call scanForDevices() before doing this + + // if they're asking for any input channels at all, use the default input, so we + // get the built-in mic rather than the built-in output with no inputs.. + + const auto selector = forInput ? kAudioHardwarePropertyDefaultInputDevice + : kAudioHardwarePropertyDefaultOutputDevice; + + if (auto deviceID = audioObjectGetProperty (kAudioObjectSystemObject, PropertyAddress (selector))) + { + auto& ids = forInput ? inputIds : outputIds; + + if (auto it = std::find (ids.begin(), ids.end(), deviceID); it != ids.end()) + { + const auto index = static_cast (std::distance (ids.begin(), it)); + YUP_MODULE_DBG (CORE_AUDIO, "Default " << (forInput ? "input" : "output") << " device resolved: index=" << String (index) << ", " << describeAudioDeviceID (*deviceID)); + return index; + } + + YUP_MODULE_DBG (CORE_AUDIO, "Default " << (forInput ? "input" : "output") << " device not found in scan: " << describeAudioDeviceID (*deviceID)); + } + + YUP_MODULE_DBG (CORE_AUDIO, "Default " << (forInput ? "input" : "output") << " device falling back to index 0"); + return 0; + } + + int getIndexOfDevice (AudioIODevice* device, bool asInput) const override + { + jassert (hasScanned); // need to call scanForDevices() before doing this + + if (auto* d = dynamic_cast (device)) + return d->getIndexOfDevice (asInput); + + return -1; + } + + bool hasSeparateInputsAndOutputs() const override { return true; } + + AudioIODevice* createDevice (const String& outputDeviceName, + const String& inputDeviceName) override + { + jassert (hasScanned); // need to call scanForDevices() before doing this + + auto inputIndex = inputDeviceNames.indexOf (inputDeviceName); + auto outputIndex = outputDeviceNames.indexOf (outputDeviceName); + + auto inputDeviceID = inputIds[inputIndex]; + auto outputDeviceID = outputIds[outputIndex]; + + YUP_MODULE_DBG (CORE_AUDIO, "createDevice requested: outputName=" << outputDeviceName << ", outputIndex=" << String (outputIndex) << ", output=" << describeAudioDeviceID (outputDeviceID) << ", inputName=" << inputDeviceName << ", inputIndex=" << String (inputIndex) << ", input=" << describeAudioDeviceID (inputDeviceID)); + + if (inputDeviceID == 0 && outputDeviceID == 0) + { + YUP_MODULE_DBG (CORE_AUDIO, "createDevice failed: no input or output device selected"); + return nullptr; + } + + const auto combinedName = outputDeviceName.isEmpty() ? inputDeviceName + : outputDeviceName; + + YUP_MODULE_DBG (CORE_AUDIO, "createDevice using CoreAudioIODevice: name=" << combinedName); + auto device = std::make_unique (this, combinedName, inputDeviceID, outputDeviceID, inputIndex, outputIndex); + + if (! device->isValid()) + { + YUP_MODULE_DBG (CORE_AUDIO, "createDevice failed: couldn't initialise CoreAudioIODevice"); + return nullptr; + } + + return device.release(); + } + + void audioDeviceListChanged() + { + scanForDevices(); + callDeviceChangeListeners(); + } + + //============================================================================== + private: + StringArray inputDeviceNames, outputDeviceNames; + Array inputIds, outputIds; + + bool hasScanned = false; + + void handleAsyncUpdate() override + { + audioDeviceListChanged(); + } + + static int getNumChannels (AudioDeviceID deviceID, bool input) + { + return getNumChannelsForAudioDevice (deviceID, input); + } + + static OSStatus hardwareListenerProc (AudioDeviceID, UInt32, const AudioObjectPropertyAddress*, void* clientData) + { + static_cast (clientData)->triggerAsyncUpdate(); + return noErr; + } + + YUP_DECLARE_WEAK_REFERENCEABLE (CoreAudioIODeviceType) + YUP_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (CoreAudioIODeviceType) + }; +}; + +} // namespace yup diff --git a/modules/yup_audio_devices/yup_audio_devices.h b/modules/yup_audio_devices/yup_audio_devices.h index c4ae9e9b..f7386f15 100644 --- a/modules/yup_audio_devices/yup_audio_devices.h +++ b/modules/yup_audio_devices/yup_audio_devices.h @@ -171,6 +171,14 @@ #define YUP_DISABLE_AUDIO_MIXING_WITH_OTHER_APPS 0 #endif +/** Config: YUP_ENABLE_CORE_AUDIO_LOGGING + + Enable logging of audio device events on macOS, such as device changes and errors. +*/ +#ifndef YUP_ENABLE_CORE_AUDIO_LOGGING +#define YUP_ENABLE_CORE_AUDIO_LOGGING 1 +#endif + //============================================================================== #include "midi_io/yup_MidiDevices.h" #include "midi_io/yup_MidiMessageCollector.h" diff --git a/modules/yup_audio_gui/device_manager/yup_AudioDeviceManagerPanel.cpp b/modules/yup_audio_gui/device_manager/yup_AudioDeviceManagerPanel.cpp new file mode 100644 index 00000000..bd98c51c --- /dev/null +++ b/modules/yup_audio_gui/device_manager/yup_AudioDeviceManagerPanel.cpp @@ -0,0 +1,232 @@ +/* + ============================================================================== + + This file is part of the YUP library. + Copyright (c) 2026 - kunitoki@gmail.com + + YUP is an open source library subject to open-source licensing. + + The code included in this file is provided under the terms of the ISC license + http://www.isc.org/downloads/software-support-policy/isc-license. Permission + to use, copy, modify, and/or distribute this software for any purpose with or + without fee is hereby granted provided that the above copyright notice and + this permission notice appear in all copies. + + YUP IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER + EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE + DISCLAIMED. + + ============================================================================== +*/ + +namespace yup +{ + +AudioDeviceManagerPanel::AudioDeviceManagerPanel (AudioDeviceManager& m) + : manager (m) +{ + outputChannelSection.setText ("Active output channels:", dontSendNotification); + inputChannelSection.setText ("Active input channels:", dontSendNotification); + + addAndMakeVisible (deviceTypeSelector); + addAndMakeVisible (deviceIOSelector); + addAndMakeVisible (outputChannelSection); + addAndMakeVisible (inputChannelSection); + addAndMakeVisible (rateBufferSelector); + addAndMakeVisible (midiSection); + + deviceTypeSelector.onTypeChanged = [this] (const String& typeName) + { + manager.setCurrentAudioDeviceType (typeName, true); + populateFromManager(); + }; + + deviceIOSelector.onDeviceChanged = [this] (const String& output, const String& input) + { + stagedSetup.outputDeviceName = output; + stagedSetup.inputDeviceName = input; + repopulateDeviceDependent(); + applyToManager(); + }; + + deviceIOSelector.onTestClicked = [this] + { + manager.playTestSound(); + }; + + outputChannelSection.onChannelsChanged = [this] (const BigInteger& active) + { + stagedSetup.outputChannels = active; + stagedSetup.useDefaultOutputChannels = false; + triggerAsyncUpdate(); + }; + + inputChannelSection.onChannelsChanged = [this] (const BigInteger& active) + { + stagedSetup.inputChannels = active; + stagedSetup.useDefaultInputChannels = false; + triggerAsyncUpdate(); + }; + + rateBufferSelector.onChanged = [this] (double rate, int bufferSize) + { + stagedSetup.sampleRate = rate; + stagedSetup.bufferSize = bufferSize; + applyToManager(); + }; + + midiSection.onInputsChanged = [this] (const StringArray& ids) + { + stagedMidiInputIds = ids; + triggerAsyncUpdate(); + }; + + midiSection.onOutputChanged = [this] (const String& id) + { + stagedMidiOutputId = id; + applyToManager(); + }; + + populateFromManager(); +} + +void AudioDeviceManagerPanel::populateFromManager() +{ + stagedSetup = manager.getAudioDeviceSetup(); + + deviceTypeSelector.populate (manager.getAvailableDeviceTypes(), + manager.getCurrentAudioDeviceType()); + + const auto& types = manager.getAvailableDeviceTypes(); + for (int i = 0; i < types.size(); ++i) + { + if (types[i]->getTypeName() == manager.getCurrentAudioDeviceType()) + { + deviceIOSelector.populate (*types[i], + stagedSetup.outputDeviceName, + stagedSetup.inputDeviceName); + break; + } + } + + repopulateDeviceDependent(); + + const auto midiInputDevices = MidiInput::getAvailableDevices(); + stagedMidiInputIds.clear(); + for (const auto& dev : midiInputDevices) + { + if (manager.isMidiInputDeviceEnabled (dev.identifier)) + stagedMidiInputIds.add (dev.identifier); + } + midiSection.populateInputs (midiInputDevices, stagedMidiInputIds); + midiSection.populateOutput (MidiOutput::getAvailableDevices(), + manager.getDefaultMidiOutputIdentifier()); + stagedMidiOutputId = manager.getDefaultMidiOutputIdentifier(); +} + +void AudioDeviceManagerPanel::repopulateDeviceDependent() +{ + AudioIODevice* deviceToQuery = nullptr; + std::unique_ptr tempDevice; + + // Fast path: use the currently open device when it matches the staged setup + if (auto* current = manager.getCurrentAudioDevice()) + { + const auto appliedSetup = manager.getAudioDeviceSetup(); + if (stagedSetup.outputDeviceName == appliedSetup.outputDeviceName + && stagedSetup.inputDeviceName == appliedSetup.inputDeviceName) + { + deviceToQuery = current; + } + } + + // Staged device differs — create a temporary device to query its properties + if (deviceToQuery == nullptr) + { + const auto& types = manager.getAvailableDeviceTypes(); + for (int i = 0; i < types.size(); ++i) + { + if (types[i]->getTypeName() != manager.getCurrentAudioDeviceType()) + continue; + + tempDevice.reset (types[i]->createDevice (stagedSetup.outputDeviceName, + stagedSetup.inputDeviceName)); + deviceToQuery = tempDevice.get(); + break; + } + } + + if (deviceToQuery != nullptr) + { + outputChannelSection.populate (deviceToQuery->getOutputChannelNames(), stagedSetup.outputChannels); + inputChannelSection.populate (deviceToQuery->getInputChannelNames(), stagedSetup.inputChannels); + rateBufferSelector.populate (deviceToQuery->getAvailableSampleRates(), stagedSetup.sampleRate, deviceToQuery->getAvailableBufferSizes(), stagedSetup.bufferSize); + } + else + { + outputChannelSection.populate ({}, {}); + inputChannelSection.populate ({}, {}); + rateBufferSelector.populate ({}, 0.0, {}, 0); + } +} + +void AudioDeviceManagerPanel::applyToManager() +{ + manager.setAudioDeviceSetup (stagedSetup, true); + + const auto midiInputDevices = MidiInput::getAvailableDevices(); + for (const auto& dev : midiInputDevices) + manager.setMidiInputDeviceEnabled (dev.identifier, + stagedMidiInputIds.contains (dev.identifier)); + + manager.setDefaultMidiOutputDevice (stagedMidiOutputId); + + // Refresh: the device may have clamped sample rate or buffer size + populateFromManager(); +} + +void AudioDeviceManagerPanel::handleAsyncUpdate() +{ + applyToManager(); +} + +MidiDeviceListConnection AudioDeviceManagerPanel::makeMidiDeviceListConnection() +{ + return MidiDeviceListConnection::make ([this] + { + populateFromManager(); + }); +} + +void AudioDeviceManagerPanel::paint (Graphics& g) +{ + g.setFillColor (findColor (DocumentWindow::Style::backgroundColorId).value_or (Colors::dimgray)); + g.fillAll(); +} + +void AudioDeviceManagerPanel::resized() +{ + auto bounds = getLocalBounds().reduced (8); + const int rowH = 44; + const int gap = 4; + const int listH = 100; + + deviceTypeSelector.setBounds (bounds.removeFromTop (rowH)); + bounds.removeFromTop (gap); + + deviceIOSelector.setBounds (bounds.removeFromTop (rowH * 2)); + bounds.removeFromTop (gap); + + outputChannelSection.setBounds (bounds.removeFromTop (20 + listH)); + bounds.removeFromTop (gap); + + inputChannelSection.setBounds (bounds.removeFromTop (20 + listH)); + bounds.removeFromTop (gap); + + rateBufferSelector.setBounds (bounds.removeFromTop (rowH * 2)); + bounds.removeFromTop (gap); + + midiSection.setBounds (bounds.removeFromTop (20 + listH + rowH)); +} + +} // namespace yup diff --git a/modules/yup_audio_gui/device_manager/yup_AudioDeviceManagerPanel.h b/modules/yup_audio_gui/device_manager/yup_AudioDeviceManagerPanel.h new file mode 100644 index 00000000..c27b24f1 --- /dev/null +++ b/modules/yup_audio_gui/device_manager/yup_AudioDeviceManagerPanel.h @@ -0,0 +1,80 @@ +/* + ============================================================================== + + This file is part of the YUP library. + Copyright (c) 2026 - kunitoki@gmail.com + + YUP is an open source library subject to open-source licensing. + + The code included in this file is provided under the terms of the ISC license + http://www.isc.org/downloads/software-support-policy/isc-license. Permission + to use, copy, modify, and/or distribute this software for any purpose with or + without fee is hereby granted provided that the above copyright notice and + this permission notice appear in all copies. + + YUP IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER + EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE + DISCLAIMED. + + ============================================================================== +*/ + +namespace yup +{ + +//============================================================================== +/** A composable panel for configuring an AudioDeviceManager. + + Changes are applied to the AudioDeviceManager immediately as the user + makes selections — no Apply button required. + + Embed in an AudioDeviceManagerWindow or add directly to any Component. +*/ +class YUP_API AudioDeviceManagerPanel : public Component + , private AsyncUpdater +{ +public: + //============================================================================== + explicit AudioDeviceManagerPanel (AudioDeviceManager& manager); + + //============================================================================== + /** Creates a MidiDeviceListConnection that keeps the MIDI device list in sync. + + The caller must hold the returned connection for as long as automatic + refresh is desired. Destroying the connection stops notifications. + AudioDeviceManagerWindow does this automatically; call it yourself when + embedding the panel directly. + */ + MidiDeviceListConnection makeMidiDeviceListConnection(); + + //============================================================================== + /** @internal */ + void paint (Graphics& g) override; + /** @internal */ + void resized() override; + +private: + void populateFromManager(); + void repopulateDeviceDependent(); + void applyToManager(); + void handleAsyncUpdate() override; + + //============================================================================== + AudioDeviceManager& manager; + + AudioDeviceManager::AudioDeviceSetup stagedSetup; + StringArray stagedMidiInputIds; + String stagedMidiOutputId; + + //============================================================================== + DeviceTypeSelector deviceTypeSelector; + DeviceIOSelector deviceIOSelector; + ChannelSection outputChannelSection; + ChannelSection inputChannelSection; + RateBufferSelector rateBufferSelector; + MidiSection midiSection; + + YUP_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (AudioDeviceManagerPanel) +}; + +} // namespace yup diff --git a/modules/yup_audio_gui/device_manager/yup_AudioDeviceManagerWindow.cpp b/modules/yup_audio_gui/device_manager/yup_AudioDeviceManagerWindow.cpp new file mode 100644 index 00000000..f06943bf --- /dev/null +++ b/modules/yup_audio_gui/device_manager/yup_AudioDeviceManagerWindow.cpp @@ -0,0 +1,46 @@ +/* + ============================================================================== + + This file is part of the YUP library. + Copyright (c) 2026 - kunitoki@gmail.com + + YUP is an open source library subject to open-source licensing. + + The code included in this file is provided under the terms of the ISC license + http://www.isc.org/downloads/software-support-policy/isc-license. Permission + to use, copy, modify, and/or distribute this software for any purpose with or + without fee is hereby granted provided that the above copyright notice and + this permission notice appear in all copies. + + YUP IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER + EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE + DISCLAIMED. + + ============================================================================== +*/ + +namespace yup +{ + +AudioDeviceManagerWindow::AudioDeviceManagerWindow (AudioDeviceManager& manager, + const ComponentNative::Options& options) + : DocumentWindow (ComponentNative::Options (options).withResizableWindow (true)) + , panel (std::make_unique (manager)) +{ + setTitle ("Audio Device Settings"); + addAndMakeVisible (*panel); + midiDeviceListConnection = panel->makeMidiDeviceListConnection(); + centreWithSize ({ 470, 680 }); +} + +void AudioDeviceManagerWindow::resized() +{ + panel->setBounds (getLocalBounds()); +} + +void AudioDeviceManagerWindow::userTriedToCloseWindow() +{ + setVisible (false); +} + +} // namespace yup diff --git a/modules/yup_audio_gui/device_manager/yup_AudioDeviceManagerWindow.h b/modules/yup_audio_gui/device_manager/yup_AudioDeviceManagerWindow.h new file mode 100644 index 00000000..f2d54623 --- /dev/null +++ b/modules/yup_audio_gui/device_manager/yup_AudioDeviceManagerWindow.h @@ -0,0 +1,52 @@ +/* + ============================================================================== + + This file is part of the YUP library. + Copyright (c) 2026 - kunitoki@gmail.com + + YUP is an open source library subject to open-source licensing. + + The code included in this file is provided under the terms of the ISC license + http://www.isc.org/downloads/software-support-policy/isc-license. Permission + to use, copy, modify, and/or distribute this software for any purpose with or + without fee is hereby granted provided that the above copyright notice and + this permission notice appear in all copies. + + YUP IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER + EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE + DISCLAIMED. + + ============================================================================== +*/ + +namespace yup +{ + +//============================================================================== +/** A DocumentWindow that hosts an AudioDeviceManagerPanel. + + Construct once and reuse. Call setVisible(true) to open; the panel's Close + button and the native close button both hide rather than destroy the window. + Staged changes are silently discarded on close. +*/ +class YUP_API AudioDeviceManagerWindow : public DocumentWindow +{ +public: + //============================================================================== + explicit AudioDeviceManagerWindow (AudioDeviceManager& manager, + const ComponentNative::Options& options = {}); + + //============================================================================== + /** @internal */ + void resized() override; + /** @internal */ + void userTriedToCloseWindow() override; + +private: + std::unique_ptr panel; + MidiDeviceListConnection midiDeviceListConnection; + + YUP_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (AudioDeviceManagerWindow) +}; + +} // namespace yup diff --git a/modules/yup_audio_gui/device_manager/yup_ChannelSection.cpp b/modules/yup_audio_gui/device_manager/yup_ChannelSection.cpp new file mode 100644 index 00000000..e4ef10ac --- /dev/null +++ b/modules/yup_audio_gui/device_manager/yup_ChannelSection.cpp @@ -0,0 +1,139 @@ +/* + ============================================================================== + + This file is part of the YUP library. + Copyright (c) 2026 - kunitoki@gmail.com + + YUP is an open source library subject to open-source licensing. + + The code included in this file is provided under the terms of the ISC license + http://www.isc.org/downloads/software-support-policy/isc-license. Permission + to use, copy, modify, and/or distribute this software for any purpose with or + without fee is hereby granted provided that the above copyright notice and + this permission notice appear in all copies. + + YUP IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER + EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE + DISCLAIMED. + + ============================================================================== +*/ + +namespace yup +{ + +//============================================================================== +ChannelSection::ChannelRowComponent::ChannelRowComponent() +{ + setOpaque (false); + + addAndMakeVisible (toggle); + + nameLabel.setColor (Label::Style::textFillColorId, Colors::black); + addAndMakeVisible (nameLabel); +} + +void ChannelSection::ChannelRowComponent::setup (const String& name, + bool active, + std::function onToggled) +{ + toggle.setButtonText ({}); + toggle.setToggleState (active, dontSendNotification); + toggle.onClick = [this, onToggled] + { + if (onToggled != nullptr) + onToggled (toggle.getToggleState()); + }; + nameLabel.setText (name, dontSendNotification); +} + +void ChannelSection::ChannelRowComponent::resized() +{ + auto bounds = getLocalBounds().reduced (2, 2); + const int toggleW = bounds.getHeight(); + toggle.setBounds (bounds.removeFromLeft (toggleW)); + bounds.removeFromLeft (4); + nameLabel.setBounds (bounds); +} + +//============================================================================== +void ChannelSection::Model::setChannels (const StringArray& names, const BigInteger& active) +{ + channelNames = names; + activeChannels = active; +} + +BigInteger ChannelSection::Model::getActiveChannels() const +{ + return activeChannels; +} + +int ChannelSection::Model::getNumRows() +{ + return channelNames.size(); +} + +Component* ChannelSection::Model::refreshComponentForRow (int rowIndex, Component* existing) +{ + auto* row = dynamic_cast (existing); + if (row == nullptr) + row = new ChannelRowComponent(); + + const bool isActive = activeChannels[rowIndex]; + + row->setup (channelNames[rowIndex], isActive, [this, rowIndex] (bool on) + { + if (on) + activeChannels.setBit (rowIndex); + else + activeChannels.clearBit (rowIndex); + + if (onChannelsChanged != nullptr) + onChannelsChanged (activeChannels); + }); + + return row; +} + +//============================================================================== +ChannelSection::ChannelSection() +{ + setOpaque (false); + + listBox.setModel (&model); + addAndMakeVisible (sectionLabel); + addAndMakeVisible (listBox); +} + +ChannelSection::~ChannelSection() = default; + +void ChannelSection::setText (const String& text, NotificationType notification) +{ + sectionLabel.setText (text, notification); +} + +void ChannelSection::populate (const StringArray& channelNames, const BigInteger& activeChannels) +{ + model.onChannelsChanged = [this] (const BigInteger& active) + { + if (onChannelsChanged != nullptr) + onChannelsChanged (active); + }; + + model.setChannels (channelNames, activeChannels); + listBox.updateContent(); +} + +BigInteger ChannelSection::getActiveChannels() const +{ + return model.getActiveChannels(); +} + +void ChannelSection::resized() +{ + auto bounds = getLocalBounds(); + sectionLabel.setBounds (bounds.removeFromTop (20)); + listBox.setBounds (bounds.reduced (2, 2)); +} + +} // namespace yup diff --git a/modules/yup_audio_gui/device_manager/yup_ChannelSection.h b/modules/yup_audio_gui/device_manager/yup_ChannelSection.h new file mode 100644 index 00000000..60fbadc4 --- /dev/null +++ b/modules/yup_audio_gui/device_manager/yup_ChannelSection.h @@ -0,0 +1,97 @@ +/* + ============================================================================== + + This file is part of the YUP library. + Copyright (c) 2026 - kunitoki@gmail.com + + YUP is an open source library subject to open-source licensing. + + The code included in this file is provided under the terms of the ISC license + http://www.isc.org/downloads/software-support-policy/isc-license. Permission + to use, copy, modify, and/or distribute this software for any purpose with or + without fee is hereby granted provided that the above copyright notice and + this permission notice appear in all copies. + + YUP IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER + EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE + DISCLAIMED. + + ============================================================================== +*/ + +namespace yup +{ + +//============================================================================== +/** A ListBox that shows audio channels as toggleable rows. + + Bit N of the BigInteger corresponds to channelNames[N]. +*/ +class YUP_API ChannelSection : public Component +{ +public: + //============================================================================== + ChannelSection(); + ~ChannelSection() override; + + //============================================================================== + /** Sets the label text displayed above the channel list. */ + void setText (const String& text, NotificationType notification = dontSendNotification); + + /** Populates the list from channel names and an active-channel bitmask. */ + void populate (const StringArray& channelNames, const BigInteger& activeChannels); + + /** Returns the current active-channel bitmask as edited by the user. */ + BigInteger getActiveChannels() const; + + //============================================================================== + /** Called when the user toggles any channel. */ + std::function onChannelsChanged; + + //============================================================================== + /** @internal */ + void resized() override; + +private: + //============================================================================== + class ChannelRowComponent : public Component + { + public: + ChannelRowComponent(); + void setup (const String& name, bool active, std::function onToggled); + void resized() override; + + private: + ToggleButton toggle; + Label nameLabel; + YUP_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (ChannelRowComponent) + }; + + //============================================================================== + class Model : public ListBoxModel + { + public: + Model() = default; + + void setChannels (const StringArray& names, const BigInteger& active); + BigInteger getActiveChannels() const; + std::function onChannelsChanged; + + int getNumRows() override; + Component* refreshComponentForRow (int rowIndex, Component* existing) override; + + private: + StringArray channelNames; + BigInteger activeChannels; + YUP_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (Model) + }; + + //============================================================================== + Label sectionLabel; + ListBox listBox; + Model model; + + YUP_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (ChannelSection) +}; + +} // namespace yup diff --git a/modules/yup_audio_gui/device_manager/yup_DeviceIOSelector.cpp b/modules/yup_audio_gui/device_manager/yup_DeviceIOSelector.cpp new file mode 100644 index 00000000..e1e1f08f --- /dev/null +++ b/modules/yup_audio_gui/device_manager/yup_DeviceIOSelector.cpp @@ -0,0 +1,104 @@ +/* + ============================================================================== + + This file is part of the YUP library. + Copyright (c) 2026 - kunitoki@gmail.com + + YUP is an open source library subject to open-source licensing. + + The code included in this file is provided under the terms of the ISC license + http://www.isc.org/downloads/software-support-policy/isc-license. Permission + to use, copy, modify, and/or distribute this software for any purpose with or + without fee is hereby granted provided that the above copyright notice and + this permission notice appear in all copies. + + YUP IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER + EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE + DISCLAIMED. + + ============================================================================== +*/ + +namespace yup +{ + +DeviceIOSelector::DeviceIOSelector() +{ + setOpaque (false); + + outputLabel.setText ("Output:", dontSendNotification); + inputLabel.setText ("Input:", dontSendNotification); + testButton.setButtonText ("Test"); + + testButton.onClick = [this] + { + if (onTestClicked != nullptr) + onTestClicked(); + }; + + addAndMakeVisible (outputLabel); + addAndMakeVisible (outputCombo); + addAndMakeVisible (testButton); + addAndMakeVisible (inputLabel); + addAndMakeVisible (inputCombo); +} + +void DeviceIOSelector::populate (AudioIODeviceType& type, + const String& currentOutput, + const String& currentInput) +{ + outputCombo.onSelectedItemChanged = nullptr; + inputCombo.onSelectedItemChanged = nullptr; + + type.scanForDevices(); + + outputCombo.clear(); + const StringArray outputNames = type.getDeviceNames (false); + for (int i = 0; i < outputNames.size(); ++i) + outputCombo.addItem (outputNames[i], i + 1); + + const int outIdx = outputNames.indexOf (currentOutput); + outputCombo.setSelectedId (outIdx >= 0 ? outIdx + 1 : 1, dontSendNotification); + + inputCombo.clear(); + const StringArray inputNames = type.getDeviceNames (true); + for (int i = 0; i < inputNames.size(); ++i) + inputCombo.addItem (inputNames[i], i + 1); + + const int inIdx = inputNames.indexOf (currentInput); + inputCombo.setSelectedId (inIdx >= 0 ? inIdx + 1 : 1, dontSendNotification); + + outputCombo.onSelectedItemChanged = [this] + { + fireDeviceChanged(); + }; + inputCombo.onSelectedItemChanged = [this] + { + fireDeviceChanged(); + }; +} + +void DeviceIOSelector::fireDeviceChanged() +{ + if (onDeviceChanged != nullptr) + onDeviceChanged (outputCombo.getText(), inputCombo.getText()); +} + +void DeviceIOSelector::resized() +{ + auto bounds = getLocalBounds(); + const int rowH = bounds.getHeight() / 2; + const int labelW = 170; + const int testW = 60; + + auto outRow = bounds.removeFromTop (rowH); + outputLabel.setBounds (outRow.removeFromLeft (labelW).reduced (0, 10)); + testButton.setBounds (outRow.removeFromRight (testW).reduced (2, 4)); + outputCombo.setBounds (outRow.reduced (2, 4)); + + inputLabel.setBounds (bounds.removeFromLeft (labelW).reduced (0, 10)); + bounds.removeFromRight (testW); + inputCombo.setBounds (bounds.reduced (2, 4)); +} + +} // namespace yup diff --git a/modules/yup_audio_gui/device_manager/yup_DeviceIOSelector.h b/modules/yup_audio_gui/device_manager/yup_DeviceIOSelector.h new file mode 100644 index 00000000..bdae8398 --- /dev/null +++ b/modules/yup_audio_gui/device_manager/yup_DeviceIOSelector.h @@ -0,0 +1,66 @@ +/* + ============================================================================== + + This file is part of the YUP library. + Copyright (c) 2026 - kunitoki@gmail.com + + YUP is an open source library subject to open-source licensing. + + The code included in this file is provided under the terms of the ISC license + http://www.isc.org/downloads/software-support-policy/isc-license. Permission + to use, copy, modify, and/or distribute this software for any purpose with or + without fee is hereby granted provided that the above copyright notice and + this permission notice appear in all copies. + + YUP IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER + EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE + DISCLAIMED. + + ============================================================================== +*/ + +namespace yup +{ + +//============================================================================== +/** Output and input device ComboBoxes with a Test button. */ +class YUP_API DeviceIOSelector : public Component +{ +public: + //============================================================================== + DeviceIOSelector(); + + //============================================================================== + /** Populates the output/input ComboBoxes from a device type. + @param type The current AudioIODeviceType (will be scanned for devices). + @param currentOutput The currently selected output device name. + @param currentInput The currently selected input device name. + */ + void populate (AudioIODeviceType& type, + const String& currentOutput, + const String& currentInput); + + //============================================================================== + /** Called when the user picks a different output or input device. */ + std::function onDeviceChanged; + + /** Called when the user clicks the Test button. */ + std::function onTestClicked; + + //============================================================================== + /** @internal */ + void resized() override; + +private: + void fireDeviceChanged(); + + Label outputLabel; + ComboBox outputCombo; + TextButton testButton; + Label inputLabel; + ComboBox inputCombo; + + YUP_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (DeviceIOSelector) +}; + +} // namespace yup diff --git a/modules/yup_audio_gui/device_manager/yup_DeviceTypeSelector.cpp b/modules/yup_audio_gui/device_manager/yup_DeviceTypeSelector.cpp new file mode 100644 index 00000000..66c41795 --- /dev/null +++ b/modules/yup_audio_gui/device_manager/yup_DeviceTypeSelector.cpp @@ -0,0 +1,64 @@ +/* + ============================================================================== + + This file is part of the YUP library. + Copyright (c) 2026 - kunitoki@gmail.com + + YUP is an open source library subject to open-source licensing. + + The code included in this file is provided under the terms of the ISC license + http://www.isc.org/downloads/software-support-policy/isc-license. Permission + to use, copy, modify, and/or distribute this software for any purpose with or + without fee is hereby granted provided that the above copyright notice and + this permission notice appear in all copies. + + YUP IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER + EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE + DISCLAIMED. + + ============================================================================== +*/ + +namespace yup +{ + +DeviceTypeSelector::DeviceTypeSelector() +{ + setOpaque (false); + + typeLabel.setText ("Driver:", dontSendNotification); + addAndMakeVisible (typeLabel); + addAndMakeVisible (typeCombo); +} + +void DeviceTypeSelector::populate (const OwnedArray& types, const String& currentType) +{ + typeCombo.onSelectedItemChanged = nullptr; + typeCombo.clear(); + + int selectedId = 1; + for (int i = 0; i < types.size(); ++i) + { + const String name = types[i]->getTypeName(); + typeCombo.addItem (name, i + 1); + if (name == currentType) + selectedId = i + 1; + } + + typeCombo.setSelectedId (selectedId, dontSendNotification); + + typeCombo.onSelectedItemChanged = [this] + { + if (onTypeChanged != nullptr) + onTypeChanged (typeCombo.getText()); + }; +} + +void DeviceTypeSelector::resized() +{ + auto bounds = getLocalBounds(); + typeLabel.setBounds (bounds.removeFromLeft (170).reduced (0, 10)); + typeCombo.setBounds (bounds.reduced (2, 4)); +} + +} // namespace yup diff --git a/modules/yup_audio_gui/device_manager/yup_DeviceTypeSelector.h b/modules/yup_audio_gui/device_manager/yup_DeviceTypeSelector.h new file mode 100644 index 00000000..a06b093d --- /dev/null +++ b/modules/yup_audio_gui/device_manager/yup_DeviceTypeSelector.h @@ -0,0 +1,55 @@ +/* + ============================================================================== + + This file is part of the YUP library. + Copyright (c) 2026 - kunitoki@gmail.com + + YUP is an open source library subject to open-source licensing. + + The code included in this file is provided under the terms of the ISC license + http://www.isc.org/downloads/software-support-policy/isc-license. Permission + to use, copy, modify, and/or distribute this software for any purpose with or + without fee is hereby granted provided that the above copyright notice and + this permission notice appear in all copies. + + YUP IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER + EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE + DISCLAIMED. + + ============================================================================== +*/ + +namespace yup +{ + +//============================================================================== +/** A labelled ComboBox for selecting the audio driver type. */ +class YUP_API DeviceTypeSelector : public Component +{ +public: + //============================================================================== + DeviceTypeSelector(); + + //============================================================================== + /** Populates the ComboBox from the available device types. + @param types The list of available device types (from AudioDeviceManager). + @param currentType The name of the currently active device type. + */ + void populate (const OwnedArray& types, const String& currentType); + + //============================================================================== + /** Called when the user selects a different driver type. */ + std::function onTypeChanged; + + //============================================================================== + /** @internal */ + void resized() override; + +private: + Label typeLabel; + ComboBox typeCombo; + + YUP_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (DeviceTypeSelector) +}; + +} // namespace yup diff --git a/modules/yup_audio_gui/device_manager/yup_MidiSection.cpp b/modules/yup_audio_gui/device_manager/yup_MidiSection.cpp new file mode 100644 index 00000000..329138a4 --- /dev/null +++ b/modules/yup_audio_gui/device_manager/yup_MidiSection.cpp @@ -0,0 +1,180 @@ +/* + ============================================================================== + + This file is part of the YUP library. + Copyright (c) 2026 - kunitoki@gmail.com + + YUP is an open source library subject to open-source licensing. + + The code included in this file is provided under the terms of the ISC license + http://www.isc.org/downloads/software-support-policy/isc-license. Permission + to use, copy, modify, and/or distribute this software for any purpose with or + without fee is hereby granted provided that the above copyright notice and + this permission notice appear in all copies. + + YUP IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER + EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE + DISCLAIMED. + + ============================================================================== +*/ + +namespace yup +{ + +//============================================================================== +MidiSection::MidiInputRowComponent::MidiInputRowComponent() +{ + setOpaque (false); + addAndMakeVisible (toggle); + addAndMakeVisible (nameLabel); +} + +void MidiSection::MidiInputRowComponent::setup (const String& name, + bool enabled, + std::function onToggled) +{ + toggle.setButtonText ({}); + toggle.setToggleState (enabled, dontSendNotification); + toggle.onClick = [this, onToggled] + { + if (onToggled != nullptr) + onToggled (toggle.getToggleState()); + }; + nameLabel.setText (name, dontSendNotification); +} + +void MidiSection::MidiInputRowComponent::resized() +{ + auto bounds = getLocalBounds().reduced (2, 2); + const int toggleW = bounds.getHeight(); + toggle.setBounds (bounds.removeFromLeft (toggleW)); + bounds.removeFromLeft (4); + nameLabel.setBounds (bounds); +} + +//============================================================================== +void MidiSection::InputModel::setDevices (const Array& devices, + const StringArray& ids) +{ + midiDevices = devices; + enabledIds = ids; +} + +StringArray MidiSection::InputModel::getEnabledIds() const +{ + return enabledIds; +} + +int MidiSection::InputModel::getNumRows() +{ + return midiDevices.size(); +} + +Component* MidiSection::InputModel::refreshComponentForRow (int rowIndex, Component* existing) +{ + auto* row = dynamic_cast (existing); + if (row == nullptr) + row = new MidiInputRowComponent(); + + const auto& dev = midiDevices[rowIndex]; + const bool enabled = enabledIds.contains (dev.identifier); + + row->setup (dev.name, enabled, [this, id = dev.identifier] (bool on) + { + if (on) + { + if (! enabledIds.contains (id)) + enabledIds.add (id); + } + else + { + enabledIds.removeString (id); + } + + if (onInputsChanged != nullptr) + onInputsChanged (enabledIds); + }); + + return row; +} + +//============================================================================== +MidiSection::MidiSection() +{ + setOpaque (false); + + inputsLabel.setText ("Active MIDI inputs:", dontSendNotification); + outputLabel.setText ("MIDI Output:", dontSendNotification); + + inputsListBox.setModel (&inputsModel); + + addAndMakeVisible (inputsLabel); + addAndMakeVisible (inputsListBox); + addAndMakeVisible (outputLabel); + addAndMakeVisible (outputCombo); +} + +MidiSection::~MidiSection() = default; + +void MidiSection::populateInputs (const Array& devices, + const StringArray& enabledIds) +{ + inputsModel.onInputsChanged = [this] (const StringArray& ids) + { + if (onInputsChanged != nullptr) + onInputsChanged (ids); + }; + + inputsModel.setDevices (devices, enabledIds); + inputsListBox.updateContent(); +} + +void MidiSection::populateOutput (const Array& devices, const String& selectedId) +{ + outputDevices = devices; + + outputCombo.onSelectedItemChanged = nullptr; + outputCombo.clear(); + outputCombo.addItem ("(none)", 1); + + int selectedComboId = 1; + for (int i = 0; i < devices.size(); ++i) + { + outputCombo.addItem (devices[i].name, i + 2); + if (devices[i].identifier == selectedId) + selectedComboId = i + 2; + } + outputCombo.setSelectedId (selectedComboId, dontSendNotification); + + outputCombo.onSelectedItemChanged = [this] + { + if (onOutputChanged == nullptr) + return; + + const int id = outputCombo.getSelectedId(); + if (id <= 1) + { + onOutputChanged ({}); + } + else + { + const int devIdx = id - 2; + if (devIdx >= 0 && devIdx < outputDevices.size()) + onOutputChanged (outputDevices[devIdx].identifier); + } + }; +} + +void MidiSection::resized() +{ + auto bounds = getLocalBounds(); + inputsLabel.setBounds (bounds.removeFromTop (20)); + inputsListBox.setBounds (bounds.removeFromTop (100).reduced (2, 2)); + + auto outRow = bounds.removeFromTop (44); + outputLabel.setBounds (outRow.removeFromLeft (170).reduced (0, 10)); + outputCombo.setBounds (outRow.reduced (2, 4)); +} + +} // namespace yup diff --git a/modules/yup_audio_gui/device_manager/yup_MidiSection.h b/modules/yup_audio_gui/device_manager/yup_MidiSection.h new file mode 100644 index 00000000..2a652a85 --- /dev/null +++ b/modules/yup_audio_gui/device_manager/yup_MidiSection.h @@ -0,0 +1,104 @@ +/* + ============================================================================== + + This file is part of the YUP library. + Copyright (c) 2026 - kunitoki@gmail.com + + YUP is an open source library subject to open-source licensing. + + The code included in this file is provided under the terms of the ISC license + http://www.isc.org/downloads/software-support-policy/isc-license. Permission + to use, copy, modify, and/or distribute this software for any purpose with or + without fee is hereby granted provided that the above copyright notice and + this permission notice appear in all copies. + + YUP IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER + EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE + DISCLAIMED. + + ============================================================================== +*/ + +namespace yup +{ + +//============================================================================== +/** MIDI input list (toggleable rows) and MIDI output selector. */ +class YUP_API MidiSection : public Component +{ +public: + //============================================================================== + MidiSection(); + ~MidiSection() override; + + //============================================================================== + /** Populates the MIDI input list. + @param devices All available MIDI input devices. + @param enabledIds Identifiers of currently-enabled MIDI inputs. + */ + void populateInputs (const Array& devices, const StringArray& enabledIds); + + /** Populates the MIDI output ComboBox. + @param devices All available MIDI output devices. + @param selectedId Identifier of the currently selected MIDI output (empty = none). + */ + void populateOutput (const Array& devices, const String& selectedId); + + //============================================================================== + /** Called when the user toggles any MIDI input. Receives the full set of enabled IDs. */ + std::function onInputsChanged; + + /** Called when the user picks a different MIDI output. */ + std::function onOutputChanged; + + //============================================================================== + /** @internal */ + void resized() override; + +private: + //============================================================================== + class MidiInputRowComponent : public Component + { + public: + MidiInputRowComponent(); + void setup (const String& name, bool enabled, std::function onToggled); + void resized() override; + + private: + ToggleButton toggle; + Label nameLabel; + YUP_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (MidiInputRowComponent) + }; + + //============================================================================== + class InputModel : public ListBoxModel + { + public: + InputModel() = default; + + void setDevices (const Array& devices, const StringArray& enabledIds); + StringArray getEnabledIds() const; + std::function onInputsChanged; + + int getNumRows() override; + Component* refreshComponentForRow (int rowIndex, Component* existing) override; + + private: + Array midiDevices; + StringArray enabledIds; + YUP_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (InputModel) + }; + + //============================================================================== + Label inputsLabel; + ListBox inputsListBox; + InputModel inputsModel; + Label outputLabel; + ComboBox outputCombo; + + Array outputDevices; + + YUP_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (MidiSection) +}; + +} // namespace yup diff --git a/modules/yup_audio_gui/device_manager/yup_RateBufferSelector.cpp b/modules/yup_audio_gui/device_manager/yup_RateBufferSelector.cpp new file mode 100644 index 00000000..1d76658b --- /dev/null +++ b/modules/yup_audio_gui/device_manager/yup_RateBufferSelector.cpp @@ -0,0 +1,105 @@ +/* + ============================================================================== + + This file is part of the YUP library. + Copyright (c) 2026 - kunitoki@gmail.com + + YUP is an open source library subject to open-source licensing. + + The code included in this file is provided under the terms of the ISC license + http://www.isc.org/downloads/software-support-policy/isc-license. Permission + to use, copy, modify, and/or distribute this software for any purpose with or + without fee is hereby granted provided that the above copyright notice and + this permission notice appear in all copies. + + YUP IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER + EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE + DISCLAIMED. + + ============================================================================== +*/ + +namespace yup +{ + +RateBufferSelector::RateBufferSelector() +{ + setOpaque (false); + + rateLabel.setText ("Sample rate:", dontSendNotification); + bufferLabel.setText ("Audio buffer size:", dontSendNotification); + + addAndMakeVisible (rateLabel); + addAndMakeVisible (rateCombo); + addAndMakeVisible (bufferLabel); + addAndMakeVisible (bufferCombo); +} + +void RateBufferSelector::populate (const Array& rates, double currentRate, const Array& bufferSizes, int currentBuffer) +{ + rateCombo.onSelectedItemChanged = nullptr; + bufferCombo.onSelectedItemChanged = nullptr; + + currentRates = rates; + currentBuffers = bufferSizes; + + rateCombo.clear(); + int selectedRateId = 1; + for (int i = 0; i < rates.size(); ++i) + { + rateCombo.addItem (String (static_cast (rates[i])) + " Hz", i + 1); + if (rates[i] == currentRate) + selectedRateId = i + 1; + } + rateCombo.setSelectedId (selectedRateId, dontSendNotification); + + bufferCombo.clear(); + int selectedBufId = 1; + for (int i = 0; i < bufferSizes.size(); ++i) + { + bufferCombo.addItem (String (bufferSizes[i]) + " samples", i + 1); + if (bufferSizes[i] == currentBuffer) + selectedBufId = i + 1; + } + bufferCombo.setSelectedId (selectedBufId, dontSendNotification); + + rateCombo.onSelectedItemChanged = [this] + { + fireChanged(); + }; + bufferCombo.onSelectedItemChanged = [this] + { + fireChanged(); + }; +} + +void RateBufferSelector::fireChanged() +{ + if (onChanged == nullptr) + return; + + const int rateIdx = rateCombo.getSelectedItemIndex(); + const int bufIdx = bufferCombo.getSelectedItemIndex(); + + if (rateIdx >= 0 && rateIdx < currentRates.size() + && bufIdx >= 0 && bufIdx < currentBuffers.size()) + { + onChanged (currentRates[rateIdx], currentBuffers[bufIdx]); + } +} + +void RateBufferSelector::resized() +{ + auto bounds = getLocalBounds(); + const int rowH = bounds.getHeight() / 2; + const int labelW = 170; // matches shared column split used by other selectors + + auto rateRow = bounds.removeFromTop (rowH); + rateLabel.setBounds (rateRow.removeFromLeft (labelW).reduced (0, 10)); + rateCombo.setBounds (rateRow.reduced (2, 4)); + + bufferLabel.setBounds (bounds.removeFromLeft (labelW).reduced (0, 10)); + bufferCombo.setBounds (bounds.reduced (2, 4)); +} + +} // namespace yup diff --git a/modules/yup_audio_gui/device_manager/yup_RateBufferSelector.h b/modules/yup_audio_gui/device_manager/yup_RateBufferSelector.h new file mode 100644 index 00000000..70185872 --- /dev/null +++ b/modules/yup_audio_gui/device_manager/yup_RateBufferSelector.h @@ -0,0 +1,64 @@ +/* + ============================================================================== + + This file is part of the YUP library. + Copyright (c) 2026 - kunitoki@gmail.com + + YUP is an open source library subject to open-source licensing. + + The code included in this file is provided under the terms of the ISC license + http://www.isc.org/downloads/software-support-policy/isc-license. Permission + to use, copy, modify, and/or distribute this software for any purpose with or + without fee is hereby granted provided that the above copyright notice and + this permission notice appear in all copies. + + YUP IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER + EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE + DISCLAIMED. + + ============================================================================== +*/ + +namespace yup +{ + +//============================================================================== +/** Sample rate and buffer size selectors. */ +class YUP_API RateBufferSelector : public Component +{ +public: + //============================================================================== + RateBufferSelector(); + + //============================================================================== + /** Populates both ComboBoxes. + @param rates Available sample rates in Hz. + @param currentRate Currently active sample rate. + @param bufferSizes Available buffer sizes in samples. + @param currentBuffer Currently active buffer size. + */ + void populate (const Array& rates, double currentRate, const Array& bufferSizes, int currentBuffer); + + //============================================================================== + /** Called when either ComboBox changes. */ + std::function onChanged; + + //============================================================================== + /** @internal */ + void resized() override; + +private: + void fireChanged(); + + Label rateLabel; + ComboBox rateCombo; + Label bufferLabel; + ComboBox bufferCombo; + + Array currentRates; + Array currentBuffers; + + YUP_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (RateBufferSelector) +}; + +} // namespace yup diff --git a/modules/yup_audio_gui/yup_audio_gui.cpp b/modules/yup_audio_gui/yup_audio_gui.cpp index 53ba9bfd..d2458dd8 100644 --- a/modules/yup_audio_gui/yup_audio_gui.cpp +++ b/modules/yup_audio_gui/yup_audio_gui.cpp @@ -43,3 +43,10 @@ #include "metering/yup_KMeterComponent.cpp" #include "graph/yup_AudioGraphNodeView.cpp" #include "graph/yup_AudioGraphComponent.cpp" +#include "device_manager/yup_DeviceTypeSelector.cpp" +#include "device_manager/yup_DeviceIOSelector.cpp" +#include "device_manager/yup_RateBufferSelector.cpp" +#include "device_manager/yup_ChannelSection.cpp" +#include "device_manager/yup_MidiSection.cpp" +#include "device_manager/yup_AudioDeviceManagerPanel.cpp" +#include "device_manager/yup_AudioDeviceManagerWindow.cpp" diff --git a/modules/yup_audio_gui/yup_audio_gui.h b/modules/yup_audio_gui/yup_audio_gui.h index e6e97171..53f26487 100644 --- a/modules/yup_audio_gui/yup_audio_gui.h +++ b/modules/yup_audio_gui/yup_audio_gui.h @@ -32,7 +32,7 @@ website: https://github.com/kunitoki/yup license: ISC - dependencies: yup_audio_basics yup_audio_formats yup_audio_processors yup_audio_graph yup_dsp yup_gui + dependencies: yup_audio_basics yup_audio_devices yup_audio_formats yup_audio_processors yup_audio_graph yup_dsp yup_gui END_YUP_MODULE_DECLARATION @@ -47,6 +47,7 @@ #include #include #include +#include #include //============================================================================== @@ -62,3 +63,10 @@ #include "metering/yup_KMeterComponent.h" #include "graph/yup_AudioGraphNodeView.h" #include "graph/yup_AudioGraphComponent.h" +#include "device_manager/yup_DeviceTypeSelector.h" +#include "device_manager/yup_DeviceIOSelector.h" +#include "device_manager/yup_RateBufferSelector.h" +#include "device_manager/yup_ChannelSection.h" +#include "device_manager/yup_MidiSection.h" +#include "device_manager/yup_AudioDeviceManagerPanel.h" +#include "device_manager/yup_AudioDeviceManagerWindow.h" diff --git a/modules/yup_core/native/yup_CFHelpers_apple.h b/modules/yup_core/native/yup_CFHelpers_apple.h index 995ec1a1..6a43b049 100644 --- a/modules/yup_core/native/yup_CFHelpers_apple.h +++ b/modules/yup_core/native/yup_CFHelpers_apple.h @@ -43,6 +43,8 @@ namespace yup { +//============================================================================== + template struct CFObjectDeleter { @@ -56,6 +58,8 @@ struct CFObjectDeleter template using CFUniquePtr = std::unique_ptr, CFObjectDeleter>; +//============================================================================== + template struct CFObjectHolder { @@ -82,4 +86,57 @@ struct CFObjectHolder CFType object = nullptr; }; +//============================================================================== +class ScopedCFArray; + +class ScopedCFDictionary +{ +public: + void setString (const String& key, const String& value) + { + const CFUniquePtr cfValue { value.toCFString() }; + setRawValue (key, cfValue.get()); + } + + void setInt (const String& key, UInt32 value) + { + const CFUniquePtr cfValue { CFNumberCreate (nullptr, kCFNumberIntType, &value) }; + setRawValue (key, cfValue.get()); + } + + void setArray (const String& key, const ScopedCFArray& array); + + CFDictionaryRef get() const noexcept { return dict.get(); } + +private: + void setRawValue (const String& key, const void* value) + { + const CFUniquePtr cfKey { key.toCFString() }; + CFDictionarySetValue (dict.get(), cfKey.get(), value); + } + + CFUniquePtr dict { CFDictionaryCreateMutable (nullptr, 0, &kCFTypeDictionaryKeyCallBacks, &kCFTypeDictionaryValueCallBacks) }; +}; + +//============================================================================== + +class ScopedCFArray +{ +public: + void appendDictionary (const ScopedCFDictionary& dictionary) + { + CFArrayAppendValue (array.get(), dictionary.get()); + } + + CFArrayRef get() const noexcept { return array.get(); } + +private: + CFUniquePtr array { CFArrayCreateMutable (nullptr, 0, &kCFTypeArrayCallBacks) }; +}; + +inline void ScopedCFDictionary::setArray (const String& key, const ScopedCFArray& arr) +{ + setRawValue (key, arr.get()); +} + } // namespace yup diff --git a/modules/yup_gui/widgets/yup_ComboBox.cpp b/modules/yup_gui/widgets/yup_ComboBox.cpp index 13ad3260..b013ca0a 100644 --- a/modules/yup_gui/widgets/yup_ComboBox.cpp +++ b/modules/yup_gui/widgets/yup_ComboBox.cpp @@ -207,10 +207,10 @@ void ComboBox::mouseDown (const MouseEvent& event) { takeKeyboardFocus(); - if (popupMenu == nullptr || ! popupMenu->isBeingShown()) - showPopup(); - else + if (isPopupShown()) hidePopup(); + else + showPopup(); repaint(); } @@ -297,6 +297,8 @@ void ComboBox::updateDisplayText() modifier.setMaxSize (textBounds.getSize()); modifier.setHorizontalAlign (StyledText::left); modifier.setVerticalAlign (StyledText::middle); + modifier.setOverflow (StyledText::ellipsis); + modifier.setWrap (StyledText::noWrap); modifier.clear(); if (displayText.isNotEmpty()) diff --git a/modules/yup_gui/widgets/yup_Label.cpp b/modules/yup_gui/widgets/yup_Label.cpp index 1dce3559..34055ef4 100644 --- a/modules/yup_gui/widgets/yup_Label.cpp +++ b/modules/yup_gui/widgets/yup_Label.cpp @@ -126,8 +126,8 @@ void Label::prepareText() if (! needsUpdate) return; - auto fontSize = getHeight() * 0.8f; // TODO - needs config - auto fontToUse = ApplicationTheme::getGlobalTheme()->getDefaultFont().withHeight (fontSize); + //auto fontSize = getHeight() * 0.8f; // TODO - needs config + auto fontToUse = ApplicationTheme::getGlobalTheme()->getDefaultFont(); // .withHeight (fontSize); if (font) fontToUse = *font; diff --git a/modules/yup_gui/yup_gui.h b/modules/yup_gui/yup_gui.h index df59d796..2e313ad2 100644 --- a/modules/yup_gui/yup_gui.h +++ b/modules/yup_gui/yup_gui.h @@ -66,7 +66,7 @@ Enable logging of windowing events like movement, resizes, mouse interactions. */ #ifndef YUP_ENABLE_GUI_WINDOWING_LOGGING -#define YUP_ENABLE_GUI_WINDOWING_LOGGING 1 +#define YUP_ENABLE_GUI_WINDOWING_LOGGING 0 #endif //============================================================================== diff --git a/tests/yup_audio_gui.cpp b/tests/yup_audio_gui.cpp index 85a29be4..220e92ce 100644 --- a/tests/yup_audio_gui.cpp +++ b/tests/yup_audio_gui.cpp @@ -28,3 +28,4 @@ #include "yup_audio_gui/yup_KMeterComponent.cpp" #include "yup_audio_gui/yup_SpectrumAnalyzerComponent.cpp" #include "yup_audio_gui/yup_SpectrogramComponent.cpp" +#include "yup_audio_gui/yup_AudioDeviceManagerPanel.cpp" diff --git a/tests/yup_audio_gui/yup_AudioDeviceManagerPanel.cpp b/tests/yup_audio_gui/yup_AudioDeviceManagerPanel.cpp new file mode 100644 index 00000000..a9850b7b --- /dev/null +++ b/tests/yup_audio_gui/yup_AudioDeviceManagerPanel.cpp @@ -0,0 +1,141 @@ +/* + ============================================================================== + + This file is part of the YUP library. + Copyright (c) 2026 - kunitoki@gmail.com + + YUP is an open source library subject to open-source licensing. + + The code included in this file is provided under the terms of the ISC license + http://www.isc.org/downloads/software-support-policy/isc-license. Permission + to use, copy, modify, and/or distribute this software for any purpose with or + without fee is hereby granted provided that the above copyright notice and + this permission notice appear in all copies. + + YUP IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER + EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE + DISCLAIMED. + + ============================================================================== +*/ + +#include + +#include + +using namespace yup; + +namespace +{ + +StringArray makeChannelNames (int count) +{ + StringArray names; + for (int i = 0; i < count; ++i) + names.add ("Channel " + String (i + 1)); + return names; +} + +} // namespace + +//============================================================================== +class ChannelSectionTests : public ::testing::Test +{ +}; + +TEST_F (ChannelSectionTests, ConstructsWithoutCrash) +{ + ChannelSection section; + EXPECT_EQ (section.getActiveChannels(), BigInteger {}); +} + +TEST_F (ChannelSectionTests, PopulateReturnsCorrectActiveMask) +{ + ChannelSection section; + BigInteger active; + active.setBit (0); + active.setBit (2); + + section.populate (makeChannelNames (4), active); + + EXPECT_EQ (section.getActiveChannels(), active); +} + +TEST_F (ChannelSectionTests, PopulateWithEmptyChannelsDoesNotCrash) +{ + ChannelSection section; + section.populate ({}, {}); + EXPECT_EQ (section.getActiveChannels(), BigInteger {}); +} + +TEST_F (ChannelSectionTests, SetTextDoesNotCrash) +{ + ChannelSection section; + section.setText ("Active output channels:", dontSendNotification); +} + +//============================================================================== +class RateBufferSelectorTests : public ::testing::Test +{ +}; + +TEST_F (RateBufferSelectorTests, ConstructsWithoutCrash) +{ + RateBufferSelector selector; + EXPECT_EQ (selector.getNumChildComponents(), 4); +} + +TEST_F (RateBufferSelectorTests, PopulateWithEmptyArraysDoesNotCrash) +{ + RateBufferSelector selector; + selector.populate ({}, 0.0, {}, 0); +} + +TEST_F (RateBufferSelectorTests, PopulateWithValidDataDoesNotCrash) +{ + RateBufferSelector selector; + selector.populate ({ 44100.0, 48000.0 }, 48000.0, { 256, 512 }, 512); +} + +//============================================================================== +class MidiSectionTests : public ::testing::Test +{ +}; + +TEST_F (MidiSectionTests, ConstructsWithoutCrash) +{ + MidiSection section; +} + +TEST_F (MidiSectionTests, PopulateInputsWithEmptyListDoesNotCrash) +{ + MidiSection section; + section.populateInputs ({}, {}); +} + +TEST_F (MidiSectionTests, PopulateOutputWithNoneSelectedDoesNotCrash) +{ + MidiSection section; + section.populateOutput ({}, {}); +} + +//============================================================================== +class AudioDeviceManagerPanelTests : public ::testing::Test +{ +}; + +TEST_F (AudioDeviceManagerPanelTests, ConstructsWithUnopenedManagerWithoutCrash) +{ + AudioDeviceManager manager; + AudioDeviceManagerPanel panel (manager); + EXPECT_GT (panel.getNumChildComponents(), 0); +} + +TEST_F (AudioDeviceManagerPanelTests, HasExpectedChildComponentCount) +{ + AudioDeviceManager manager; + AudioDeviceManagerPanel panel (manager); + // 6 direct children: type selector, IO selector, 2x channel section, + // rate+buffer selector, MIDI section + EXPECT_EQ (panel.getNumChildComponents(), 6); +}