diff options
-rw-r--r-- | util/macos_test_audio_config/CMakeLists.txt | 13 | ||||
-rw-r--r-- | util/macos_test_audio_config/README | 25 | ||||
-rwxr-xr-x | util/macos_test_audio_config/compile_and_run.sh | 19 | ||||
-rw-r--r-- | util/macos_test_audio_config/main.cpp | 498 |
4 files changed, 555 insertions, 0 deletions
diff --git a/util/macos_test_audio_config/CMakeLists.txt b/util/macos_test_audio_config/CMakeLists.txt new file mode 100644 index 000000000..d99987865 --- /dev/null +++ b/util/macos_test_audio_config/CMakeLists.txt @@ -0,0 +1,13 @@ +# Copyright (C) 2022 The Qt Company Ltd. +# SPDX-License-Identifier: BSD-3-Clause + +cmake_minimum_required(VERSION 3.5) + +project(test_audio_config LANGUAGES CXX) + +set(CMAKE_CXX_STANDARD 17) +set(CMAKE_CXX_STANDARD_REQUIRED ON) + +add_executable(test_audio_config main.cpp) + +target_link_libraries(test_audio_config "-framework Coreaudio" "-framework CoreFoundation") diff --git a/util/macos_test_audio_config/README b/util/macos_test_audio_config/README new file mode 100644 index 000000000..560d84100 --- /dev/null +++ b/util/macos_test_audio_config/README @@ -0,0 +1,25 @@ +This utility tests stability of audio configuration on macOS, and prints it. +It reads audio config via AudioObjectGetPropertyData many times, checks if it changes, and dumps results. +It might be useful to check the configuration if specific audio devices are installed on your PC +and Qt multimedia handles them incorrectly or writes warnings to the console. +It's a good idea to attach a text file with the utility's output when creating a bug for audio on macOS. +In order to test an unstable config, set some sufficient testing time and just attach/detach a device. + + +Build: + +mkdir build +cd build +cmake .. +make + + +Run: + +./test_audio_config +Running without parameters performs some default testing. Use --help to see customization details. + + +Lazy build, run, and dump the result to file test_audio_config_res.txt: + +./compile_and_run.sh diff --git a/util/macos_test_audio_config/compile_and_run.sh b/util/macos_test_audio_config/compile_and_run.sh new file mode 100755 index 000000000..6cd477769 --- /dev/null +++ b/util/macos_test_audio_config/compile_and_run.sh @@ -0,0 +1,19 @@ +#! /bin/sh +# Copyright (C) 2019 The Qt Company Ltd. +# SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only + +BUILD_DIR="__build__" +OUTPUT_FILE="test_audio_config_res.txt" + +mkdir $BUILD_DIR +cd $BUILD_DIR +cmake .. +make -j +cd .. + +rm -f $OUTPUT_FILE +./$BUILD_DIR/test_audio_config "$@" >> $OUTPUT_FILE +cat $OUTPUT_FILE +echo "\nThe result has been written to file $OUTPUT_FILE\n" + +rm -rf $BUILD_DIR diff --git a/util/macos_test_audio_config/main.cpp b/util/macos_test_audio_config/main.cpp new file mode 100644 index 000000000..43892b4a7 --- /dev/null +++ b/util/macos_test_audio_config/main.cpp @@ -0,0 +1,498 @@ +// Copyright (C) 2022 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only + +#include <iostream> +#include <vector> +#include <optional> +#include <unordered_map> +#include <sstream> +#include <thread> +#include <chrono> +#include <algorithm> + +#include <CoreAudio/AudioHardware.h> + +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<typename T> +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<const char *>(&code.code); + + std::copy_n(std::make_reverse_iterator(desc + sizeof(code.code)), sizeof(code.code), + std::ostream_iterator<char>(os)); + + os << '}'; + + return os; +} + +ostream &operator<<(ostream &os, const LogOffset &offset) +{ + std::fill_n(std::ostream_iterator<char>(os), offset.value * LogOffsetSize, ' '); + + return os; +} + +template<typename T> +ostream &operator<<(ostream &os, const Span<T> &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<char> localBuffer(maxLengthInUtf8); + + if (CFStringGetCString(str, localBuffer.data(), maxLengthInUtf8, maxLengthInUtf8)) + os << localBuffer.data(); + else + os << "{empty}"; + } + + return os; +} + +} // namespace std + +template<typename T = char> +static std::optional<std::vector<T>> +getAudioData(std::ostream &os, const LogOffset &offset, AudioObjectID inObjectID, + const AudioObjectPropertyAddress &inAddress, size_t minDataSize = 0) +{ + static_assert(std::is_trivial_v<T>, "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<T> 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<std::uint32_t>(res) } << '\n'; + } else { + os << offset << "AudioObjectGetPropertyDataSize failed, Err: " + << Code{ static_cast<std::uint32_t>(res) } << '\n'; + } + + return {}; +} + +template<typename T = char> +static std::optional<T> getAudioObject(std::ostream &os, const LogOffset &offset, + AudioObjectID inObjectID, + const AudioObjectPropertyAddress &inAddress) +{ + if (auto data = getAudioData<T>(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<AudioStreamID>(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<AudioStreamBasicDescription>( + 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<AudioStreamRangedDescription>( + 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<AudioChannelLayout *>(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<Float32>{ 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<AudioStreamBasicDescription>( + 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<CFStringRef>(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<UInt32>(os, offset + 2, id, propertyAddress)) + os << offset + 2 << std::boolalpha << static_cast<bool>(*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<UInt32>(os, offset + 2, id, propertyAddress, 2)) + os << offset + 2 << Span<UInt32>{ 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<AudioDeviceID>(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<std::string, std::uint32_t> 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<std::reference_wrapper<Result>> 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<int> { + 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; +} |