summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--util/macos_test_audio_config/CMakeLists.txt13
-rw-r--r--util/macos_test_audio_config/README25
-rwxr-xr-xutil/macos_test_audio_config/compile_and_run.sh19
-rw-r--r--util/macos_test_audio_config/main.cpp498
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;
+}