diff options
Diffstat (limited to 'util')
-rw-r--r-- | util/adt_generate_qt.m | 165 | ||||
-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 |
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; +} |