// Copyright (C) 2022 The Qt Company Ltd. // SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0 #include #include #include #include #include #include #include #include #include static constexpr std::uint32_t ElementMaster = kAudioObjectPropertyElementMaster; // TODO: use kAudioObjectPropertyElementMain for macOS // versions >= 12.0 static constexpr std::uint32_t LogOffsetSize = 2; static constexpr std::chrono::milliseconds DefaultTestingTime(1000); static constexpr std::chrono::milliseconds DefaultTestingInterval(0); struct Flags { std::uint32_t value = 0; }; struct Code { std::uint32_t code = 0; }; template struct Span { const T *data = nullptr; size_t size = 0; }; struct LogOffset { std::uint32_t value = 0; LogOffset operator+(std::uint32_t v) const { return LogOffset{ value + v }; } }; namespace std { ostream &operator<<(ostream &os, const Flags &flags) { os << '['; std::uint32_t index = 0; bool first = true; for (auto value = flags.value; value; value >>= 1, ++index) { if ((value & 1) != 0) { if (!first) os << ','; os << index; first = false; } } os << ']'; return os; } ostream &operator<<(ostream &os, const Code &code) { os << '{' << code.code << '|'; const char *desc = reinterpret_cast(&code.code); std::copy_n(std::make_reverse_iterator(desc + sizeof(code.code)), sizeof(code.code), std::ostream_iterator(os)); os << '}'; return os; } ostream &operator<<(ostream &os, const LogOffset &offset) { std::fill_n(std::ostream_iterator(os), offset.value * LogOffsetSize, ' '); return os; } template ostream &operator<<(ostream &os, const Span &span) { os << '['; if (span.size) os << span.data[0]; for (size_t i = 1; i < span.size; ++i) os << ',' << span.data[i]; os << ']'; return os; } ostream &operator<<(ostream &os, const CFStringRef &str) { const auto originalBuffer = CFStringGetCStringPtr(str, kCFStringEncodingUTF8); if (originalBuffer) { os << originalBuffer; } else { const auto lengthInUtf16 = CFStringGetLength(str); const auto maxLengthInUtf8 = CFStringGetMaximumSizeForEncoding(lengthInUtf16, kCFStringEncodingUTF8) + 1; std::vector localBuffer(maxLengthInUtf8); if (CFStringGetCString(str, localBuffer.data(), maxLengthInUtf8, maxLengthInUtf8)) os << localBuffer.data(); else os << "{empty}"; } return os; } } // namespace std template static std::optional> getAudioData(std::ostream &os, const LogOffset &offset, AudioObjectID inObjectID, const AudioObjectPropertyAddress &inAddress, size_t minDataSize = 0) { static_assert(std::is_trivial_v, "Trivial type is expected"); UInt32 propSize = 0; const auto res = AudioObjectGetPropertyDataSize(inObjectID, &inAddress, 0, nullptr, &propSize); if (res == noErr) { if (propSize / sizeof(T) < minDataSize) { os << offset << "Data size is too low: actual " << propSize << "B vs expected " << minDataSize * sizeof(T) << "B\n"; return {}; } std::vector data(propSize / sizeof(T)); if (data.size() * sizeof(T) != propSize) os << offset << "Probably, wrong data size: " << propSize << ", Element size: " << sizeof(T) << '\n'; const auto res = AudioObjectGetPropertyData(inObjectID, &inAddress, 0, nullptr, &propSize, data.data()); if (res == noErr) return { std::move(data) }; else os << offset << "AudioObjectGetPropertyData failed, Err: " << Code{ static_cast(res) } << '\n'; } else { os << offset << "AudioObjectGetPropertyDataSize failed, Err: " << Code{ static_cast(res) } << '\n'; } return {}; } template static std::optional getAudioObject(std::ostream &os, const LogOffset &offset, AudioObjectID inObjectID, const AudioObjectPropertyAddress &inAddress) { if (auto data = getAudioData(os, offset, inObjectID, inAddress, 1)) { if (data->size() > 1) os << offset << "Warn: unexpected data size: " << data->size() << '\n'; return { data->front() }; } return {}; } static void dumpFormats(std::ostream &os, const LogOffset &offset, AudioDeviceID id, std::uint32_t scope) { os << offset << "Formats:\n"; const AudioObjectPropertyAddress audioDevicePropertyStreamsAddress{ kAudioDevicePropertyStreams, scope, ElementMaster }; if (auto streamIDs = getAudioData(os, offset + 1, id, audioDevicePropertyStreamsAddress)) { for (auto &streamID : *streamIDs) { os << offset + 1 << "Stream id: " << streamID << '\n'; auto dumpCurrentFormat = [&](std::uint32_t selector) { const AudioObjectPropertyAddress propertyAddress{ selector, scope, ElementMaster }; if (auto format = getAudioObject( os, offset + 3, streamID, propertyAddress)) { os << offset + 3 << "mFormatID: " << format->mFormatID << '\n'; os << offset + 3 << "mSampleRate: " << format->mSampleRate << '\n'; os << offset + 3 << "mFormatFlags: " << Flags{ format->mFormatFlags } << '\n'; } }; auto dumpAvailableFormats = [&](std::uint32_t selector) { const AudioObjectPropertyAddress propertyAddress{ selector, scope, ElementMaster }; if (auto descriptions = getAudioData( os, offset + 3, streamID, propertyAddress)) { size_t index = 0; for (const AudioStreamRangedDescription &desc : *descriptions) { const auto &format = desc.mFormat; os << offset + 3 << "mFormatID: " << format.mFormatID << " (Index " << index << ")\n"; os << offset + 4 << "mSampleRateRange: [" << desc.mSampleRateRange.mMinimum << "; " << desc.mSampleRateRange.mMaximum << "]\n" << offset + 4 << "mSampleRate: " << format.mSampleRate << '\n' << offset + 4 << "mFormatFlags: " << Flags{ format.mFormatFlags } << '\n' << offset + 4 << "mBytesPerPacket: " << format.mBytesPerPacket << '\n' << offset + 4 << "mFramesPerPacket: " << format.mFramesPerPacket << '\n' << offset + 4 << "mBytesPerFrame: " << format.mBytesPerFrame << '\n' << offset + 4 << "mChannelsPerFrame: " << format.mChannelsPerFrame << '\n' << offset + 4 << "mBitsPerChannel: " << format.mBitsPerChannel << '\n'; ++index; } } }; os << offset + 2 << "Preffered physical format " << Code{ kAudioStreamPropertyPhysicalFormat } << ":\n"; dumpCurrentFormat(kAudioStreamPropertyPhysicalFormat); os << offset + 2 << "Available physical formats " << Code{ kAudioStreamPropertyAvailablePhysicalFormats } << ":\n"; dumpAvailableFormats(kAudioStreamPropertyAvailablePhysicalFormats); os << offset + 2 << "Preffered virtual format " << Code{ kAudioStreamPropertyVirtualFormat } << ":\n"; dumpCurrentFormat(kAudioStreamPropertyVirtualFormat); os << offset + 2 << "Available virtual formats " << Code{ kAudioStreamPropertyAvailableVirtualFormats } << ":\n"; dumpAvailableFormats(kAudioStreamPropertyAvailableVirtualFormats); } } } static void dumpChannelsLayout(std::ostream &os, const LogOffset &offset, AudioDeviceID id, std::uint32_t scope) { os << offset << "Channels Layout " << Code{ kAudioDevicePropertyPreferredChannelLayout } << ":\n"; const AudioObjectPropertyAddress audioDeviceChannelLayoutPropertyAddress{ kAudioDevicePropertyPreferredChannelLayout, scope, ElementMaster }; if (auto data = getAudioData(os, offset + 1, id, audioDeviceChannelLayoutPropertyAddress, sizeof(AudioChannelLayout))) { const AudioChannelLayout &layout = *reinterpret_cast(data->data()); os << offset + 1 << "mChannelLayoutTag: " << layout.mChannelLayoutTag << "\n" << offset + 1 << "mChannelBitmap: " << Flags{ layout.mChannelBitmap } << "\n" << offset + 1 << "mNumberChannelDescriptions: " << layout.mNumberChannelDescriptions << "\n" << offset + 1 << "ChannelDescriptions:\n"; for (UInt32 i = 0; i < layout.mNumberChannelDescriptions; ++i) { const auto &desc = layout.mChannelDescriptions[i]; os << offset + 2 << "Channel " << i << ":\n"; os << offset + 3 << "mChannelLabel: " << desc.mChannelLabel; if (desc.mChannelLabel == 0xFFFFFFFF) os << " (unknown)"; os << '\n'; os << offset + 3 << "mChannelFlags: " << Flags{ desc.mChannelFlags } << '\n' << offset + 3 << "mCoordinates: " << Span{ desc.mCoordinates, 3 } << '\n'; } } } static void dumpBasicDescription(std::ostream &os, const LogOffset &offset, AudioDeviceID id, std::uint32_t scope) { os << offset << "Basic Description " << Code{ kAudioDevicePropertyStreamFormat } << ":\n"; const AudioObjectPropertyAddress audioDeviceStreamFormatPropertyAddress{ kAudioDevicePropertyStreamFormat, scope, ElementMaster }; if (auto basicDescr = getAudioObject( os, offset + 1, id, audioDeviceStreamFormatPropertyAddress)) { os << offset + 1 << "mSampleRate: " << basicDescr->mSampleRate << "\n" << offset + 1 << "mFormatID: " << basicDescr->mFormatID << "\n" << offset + 1 << "mFormatFlags: " << Flags{ basicDescr->mFormatFlags } << "\n" << offset + 1 << "mBytesPerPacket: " << basicDescr->mBytesPerPacket << "\n" << offset + 1 << "mFramesPerPacket: " << basicDescr->mFramesPerPacket << "\n" << offset + 1 << "mBytesPerFrame: " << basicDescr->mBytesPerFrame << "\n" << offset + 1 << "mChannelsPerFrame: " << basicDescr->mChannelsPerFrame << "\n" << offset + 1 << "mBitsPerChannel: " << basicDescr->mBitsPerChannel << "\n"; } } static void dumpGeneralDeviceInfo(std::ostream &os, const LogOffset &offset, AudioDeviceID id, std::uint32_t scope) { os << offset << "General Device Info:\n"; auto dumpString = [&](const char *name, std::uint32_t selector) { os << offset + 1 << name << " " << Code{ selector } << ":\n"; const AudioObjectPropertyAddress propertyAddress{ selector, scope, ElementMaster }; if (auto str = getAudioObject(os, offset + 2, id, propertyAddress)) { os << offset + 2 << *str << '\n'; CFRelease(*str); } }; auto dumpBool = [&](const char *name, std::uint32_t selector) { os << offset + 1 << name << ' ' << Code{ selector } << ":\n"; const AudioObjectPropertyAddress propertyAddress{ selector, scope, ElementMaster }; if (auto value = getAudioObject(os, offset + 2, id, propertyAddress)) os << offset + 2 << std::boolalpha << static_cast(*value) << '\n'; }; dumpString("Name", kAudioObjectPropertyName); dumpString("Manufacturer", kAudioObjectPropertyManufacturer); dumpString("Element name", kAudioObjectPropertyElementName); dumpString("Model name", kAudioObjectPropertyModelName); dumpBool("Device is alive", kAudioDevicePropertyDeviceIsAlive); dumpBool("Device is running", kAudioDevicePropertyDeviceIsRunning); dumpBool("Can be default device", kAudioDevicePropertyDeviceCanBeDefaultDevice); dumpBool("Can be default system device", kAudioDevicePropertyDeviceCanBeDefaultSystemDevice); dumpBool("Device property is hidden", kAudioDevicePropertyIsHidden); { const AudioObjectPropertyAddress propertyAddress{ kAudioDevicePropertyPreferredChannelsForStereo, scope, ElementMaster }; os << offset + 1 << "Preffered channels for stereo " << Code{ kAudioDevicePropertyPreferredChannelsForStereo } << ":\n"; if (auto data = getAudioData(os, offset + 2, id, propertyAddress, 2)) os << offset + 2 << Span{ data->data(), 2 } << '\n'; } } static void dumpAvailableAudioDevices(std::ostream &os, const LogOffset &offset, std::uint32_t scope) { os << offset << "Dump devices " << Code{ kAudioHardwarePropertyDevices } << ", scope: " << Code{ scope } << "\n"; const AudioObjectPropertyAddress audioDevicesPropertyAddress{ kAudioHardwarePropertyDevices, scope, ElementMaster }; if (auto devices = getAudioData(os, offset + 1, kAudioObjectSystemObject, audioDevicesPropertyAddress)) { size_t index = 0; for (auto id : *devices) { os << offset + 2 << "ID: " << id << " (Index " << index << ")\n"; dumpGeneralDeviceInfo(os, offset + 3, id, scope); dumpBasicDescription(os, offset + 3, id, scope); dumpChannelsLayout(os, offset + 3, id, scope); dumpFormats(os, offset + 3, id, scope); ++index; } } } static void dumpAvailableAudioDevices(std::ostream &os, const LogOffset &offset) { for (auto scope : { kAudioObjectPropertyScopeInput, kAudioObjectPropertyScopeOutput }) { dumpAvailableAudioDevices(os, offset, scope); os << offset << "\n"; } } static void testStability(const std::chrono::milliseconds &time, const std::chrono::milliseconds &interval) { std::cout << "Start testing audio config...\n" << std::endl; std::unordered_map results; std::uint32_t counter = 0; const auto end = std::chrono::system_clock::now() + time; while (true) { std::ostringstream stream; dumpAvailableAudioDevices(stream, {}); ++counter; ++results[stream.str()]; if (std::chrono::system_clock::now() + interval >= end) break; std::this_thread::sleep_for(interval); } std::cout << "Audio config has been tested for " << time.count() << "ms\n" << "Set --time %your time% in order to change the testing time.\n" << "The config has been taken " << counter << " times\n" << "------------------------------------------------------------\n"; std::cout << std::endl; using Result = decltype(results)::value_type; std::vector> resultRefs(results.begin(), results.end()); std::sort(resultRefs.begin(), resultRefs.end(), [](const Result &a, const Result &b) { return a.second < b.second; }); for (size_t i = 0; i < resultRefs.size(); ++i) { const Result &res = resultRefs[i]; std::cout << "Result N" << i + 1 << "; Occurs: " << res.second << '/' << counter << '\n' << res.first; } if (results.size() == 1) std::cout << "The config seems to be stable (" << counter << " times)\n"; else std::cout << "The config is unstable: " << results.size() << " different results\n"; std::cout << "\nTesting done!" << std::endl; } static void printHelp() { // clang-format off std::cout << "This utility tests stability of audio configuration on macOS, and prints it.\n" "It reads audio config via AudioObjectGetPropertyData many times, checks if it changes and dumps results.\n" "It might be useful to check the configuration if specific audio devices are installed on your PC\n" "and Qt multimedia handles them incorrectly or writes warnings to the console.\n" "It's a good idea to attach a text file with the utility's output when creating a bug for audio on macOS.\n" "In order to test an unstable config, set some sufficient testing time and just attach/detach a device.\n" "Options:\n" " --time Common time of testing in ms. Defaults to " << DefaultTestingTime.count() << ".\n" " Set --time 0 for simple duming of the audio config.\n" " --interval Interval in ms between reading of audio config. Defaults to " << DefaultTestingInterval.count() << ".\n" " --help Show help."; // clang-format on std::cout << std::endl; } int main(int argc, char *argv[]) { std::chrono::milliseconds testingTime = DefaultTestingTime; std::chrono::milliseconds testingInterval = DefaultTestingInterval; for (int i = 1; i < argc; ++i) { auto getIntValue = [&]() -> std::optional { if (i + 1 < argc) { char *end = argv[i + 1]; const auto val = strtol(argv[i + 1], &end, 10); ++i; if (*end == '\0') return { val }; else std::cout << "Cannot read value " << argv[i + 1] << std::endl; } else { std::cout << "Cannot read value" << std::endl; } return {}; }; if (strcmp(argv[i], "--time") == 0) { if (auto time = getIntValue()) testingTime = std::chrono::milliseconds(*time); else return 1; } else if (strcmp(argv[i], "--interval") == 0) { if (auto interval = getIntValue()) testingInterval = std::chrono::milliseconds(*interval); else return 1; } else if (strcmp(argv[i], "--help") == 0) { printHelp(); return 0; } else { std::cout << "Wrong option " << argv[i] << std::endl; return 1; } } testStability(testingTime, testingInterval); return 0; }