summaryrefslogtreecommitdiffstats
path: root/util
diff options
context:
space:
mode:
Diffstat (limited to 'util')
-rw-r--r--util/adt_generate_qt.m165
-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
5 files changed, 720 insertions, 0 deletions
diff --git a/util/adt_generate_qt.m b/util/adt_generate_qt.m
new file mode 100644
index 000000000..453732273
--- /dev/null
+++ b/util/adt_generate_qt.m
@@ -0,0 +1,165 @@
+%{
+** Copyright (C) 2021 The Qt Company Ltd.
+** SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0
+%}
+
+% Create mapping matrixes to convert ambisonic to different speaker layouts supported in Qt Multimedia
+%
+% This uses the ADT octave framework from https://bitbucket.org/ambidecodertoolbox/adt/src/master/
+% to generate conversion matrixes between ambisonic formats and various speaker configurations
+%
+% We're generating two band conversion matrices with a cutoff frequency of 380 Herz. Filtering low
+% and high frequencies differently is important to get a decent spatial reproduction. For details
+% see the "Is my decoder Ambisonic?" paper (https://ambisonics.dreamhosters.com/BLaH3.pdf)
+%
+function adt_generate_qt()
+ % assume speakers are 2 meters from listener. This should avoid near-field
+ % effects and should work for most room setups
+ radius = 2;
+
+ [outfile,msg] = fopen("qambisonicdecoderdata_p.h",'w');
+ fprintf(outfile, "// Copyright (C) 2016 The Qt Company Ltd.\n");
+ fprintf(outfile, "//SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only\n");
+ fprintf(outfile, "#ifndef QAMBISONICDECODERDATA_P_H\n");
+ fprintf(outfile, "#define QAMBISONICDECODERDATA_P_H\n\n");
+ fprintf(outfile, "#include <qtspatialaudioglobal_p.h>\n\n");
+ fprintf(outfile, "// W A R N I N G\n");
+ fprintf(outfile, "// -------------\n");
+ fprintf(outfile, "//\n");
+ fprintf(outfile, "// This file is not part of the Qt API. It exists purely as an\n");
+ fprintf(outfile, "// implementation detail. This header file may change from version to\n");
+ fprintf(outfile, "// version without notice, or even be removed.\n");
+ fprintf(outfile, "//\n");
+ fprintf(outfile, "// We mean it.\n");
+ fprintf(outfile, "//\n\n");
+ fprintf(outfile, "// This file is generated by the matlab/octave file adt_generate_qt.m\n");
+ fprintf(outfile, "// using the Ambisonic Decoder Toolbox (https://bitbucket.org/ambidecodertoolbox/adt/src/master/)\n");
+ fprintf(outfile, "\n\n");
+ fprintf(outfile, "QT_BEGIN_NAMESPACE\n\n");
+
+ % cover top/bottom for surround
+ imag_speakers = [0,0,radius; 0,0,-radius];
+
+ % https://www.dolby.com/us/en/guide/surround-sound-speaker-setup/5-1-setup.html
+ % Dolby 5.1 -- F: 22-30, S: 110-120, C in same plane as F
+ S = ambi_spkr_array(...
+ '5dot0', ...
+ 'AER', 'DDM', ...
+ 'L', [ 30, 0, radius], ...
+ 'R', [ -30, 0, radius], ...
+ 'C', [ 0, 0, radius], ...
+ 'Ls', [ 110, 0, radius], ...
+ 'Rs', [-110, 0, radius] ...
+ );
+ createDecoders(S, imag_speakers, outfile);
+
+ S.lfeRow = 4;
+ S.name = "5dot1";
+ createDecoders(S, imag_speakers, outfile);
+
+ % https://www.dolby.com/us/en/guide/surround-sound-speaker-setup/7-1-setup.html
+ % Dolby 7.1 F: 22-30, S: 90-110, B: 135-150
+ S = ambi_spkr_array(...
+ ... % array name
+ '7dot0', ...
+ ... % coordinate codes, unit codes
+ ... % Azimuth, Elevation, Radius; Degrees, Degrees, Meters
+ 'AER', 'DDM', ...
+ ... % speaker name, [azimuth, elevation, radius]
+ 'L', [ 30, 0, radius], ...
+ 'R', [ -30, 0, radius], ...
+ 'C', [ 0, 0, radius], ...
+ 'Ls', [ 90, 0, radius], ...
+ 'Rs', [ -90, 0, radius], ...
+ 'Lb', [ 150, 0, radius], ...
+ 'Rb', [-150, 0, radius] ...
+ );
+ createDecoders(S, imag_speakers, outfile);
+
+ S.lfeRow = 4;
+ S.name = "7dot1";
+ createDecoders(S, imag_speakers, outfile);
+
+ fprintf(outfile, "QT_END_NAMESPACE\n\n");
+ fprintf(outfile, "#endif\n\n");
+ fclose(outfile);
+end
+
+% remove rounding errors
+function m = trimMatrix(m)
+ for i = 1:rows(m)
+ for j = 1:columns(m)
+ if (abs(m(i, j)) < 1e-4)
+ m(i, j) = 0;
+ endif
+ endfor
+ endfor
+end
+
+function writeLFERow(outfile, m, suffix)
+ if (strcmp(suffix, 'hf'))
+ fprintf(outfile, "0.0f, "); % no need for High frequency data on the LFE channel
+ else
+ fprintf(outfile, "0.5f, ");
+ endif
+ for i = 2:columns(m)
+ fprintf(outfile, "0.0f, ");
+ endfor
+ fprintf(outfile, "// LFE\n");
+end
+
+function [n, m] = getnm(l)
+% Computes spherical harmonic degree and order from Ambisonic Channel Number.
+ n = floor(sqrt(l));
+ m = l-n.^2-n;
+end
+
+function channels = normalizeSN3D(channels)
+ for i = 1:columns(channels)
+ [n, m] = getnm(i-1);
+ if (m == 0)
+ factor = 1;
+ else
+ factor = sqrt(2 * factorial(n - abs(m)) / (factorial(n + abs(m))));
+ endif
+ % There's an adjustment factor of 1.5 here, to match the volumes generated by the matrices
+ % here with what we get from a simple stereo decoding. The factor is heuristically determined
+ channels(i) *= factor*1.5;
+ endfor
+end
+
+function writeMatrix(outfile, level, S, M, suffix)
+ m = trimMatrix(M);
+ hasLFE = isfield(S, "lfeRow");
+ r = rows(m);
+ c = columns(m);
+ if (hasLFE)
+ r = r + 1;
+ endif
+ fprintf(outfile, "// Decoder matrix for %s, ambisonic level %d\n", S.name, level);
+ fprintf(outfile, "static constexpr float decoderMatrix_%s_%d_%s[%d*%d] = {\n", S.name, level, suffix, r, c);
+ for i = 1:rows(S.id)
+ channels = normalizeSN3D(m(i, :))
+ fprintf(outfile, "%ff, ", channels);
+ fprintf(outfile, "// %s\n", S.id(i, 1){1});
+ if (hasLFE && S.lfeRow == i + 1)
+ writeLFERow(outfile, m, suffix);
+ endif
+ endfor
+ fprintf(outfile, "};\n\n");
+end
+
+function createOneDecoder(S, imag_speakers, outfile, level)
+ ambi_order = ambi_channel_definitions_convention(level, 'ambix2011')
+ [D,S,M,C] = ambi_run_allrad(S, ambi_order, imag_speakers, [S.name '_' int2str(level)], false, "amb", 1, 3);
+ writeMatrix(outfile, level, S, M.lf, "lf");
+ m = ambi_apply_gamma(M.hf, D.hf_gains, C);
+ writeMatrix(outfile, level, S, m, "hf");
+end
+
+function createDecoders(S, imag_speakers, outfile)
+ for level = [1:3]
+ createOneDecoder(S, imag_speakers, outfile, level)
+ endfor
+end
+
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..8d9afcf43
--- /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 WITH Qt-GPL-exception-1.0
+
+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..4b1fc1702
--- /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 GPL-3.0-only WITH Qt-GPL-exception-1.0
+
+#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;
+}