Revision control

++ // Copyright (C) 2026 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 "qohosaudiodevices_p.h"
++
++ #include "qohosaudiodevice_p.h"
++ #include "qohosaudiosink_p.h"
++ #include "qohosaudiosource_p.h"
++
++ #include <private/qaudiodevice_p.h>
++ #include <private/qaudioformat_p.h>
++
++ #include <QtCore/qloggingcategory.h>
++ #include <QtCore/qmutex.h>
++ #include <QtCore/qspan.h>
++
++ #include <ohaudio/native_audio_device_base.h>
++ #include <ohaudio/native_audio_manager.h>
++ #include <ohaudio/native_audio_routing_manager.h>
++ #include <ohaudio/native_audiostream_base.h>
++
++ #include <optional>
++
++ QT_BEGIN_NAMESPACE
++
++ namespace {
++
++ Q_STATIC_LOGGING_CATEGORY(qLcOhosAudioDevices, "qt.multimedia.ohos.audiodevices")
++
++ // OH_AudioRoutingManager_OnDeviceChangedCallback carries no user data, so the
++ // device-change callbacks reach the (single) QOhosAudioDevices instance through
++ // this pointer, set for the lifetime of the object.
++ Q_CONSTINIT QOhosAudioDevices *g_audioDevicesInstance = nullptr;
++
++ // Serializes access to g_audioDevicesInstance so a device-change callback
++ // arriving on an OHAudio thread cannot run against a half-destroyed instance.
++ Q_CONSTINIT QBasicMutex g_callbackMutex;
++
++ struct DeviceFormatInfo
++ {
++ QAudioFormat preferredFormat;
++ int maximumChannelCount = 0;
++ };
++
++ DeviceFormatInfo deviceFormatInfo(OH_AudioDeviceDescriptor *descriptor)
++ {
++ namespace ranges = QtMultimediaPrivate::ranges;
++
++ DeviceFormatInfo info;
++ QAudioFormat &format = info.preferredFormat;
++
++ // OHAudio reports INVALID_PARAM for the channel counts (and sometimes the
++ // sample rates) of otherwise valid devices such as USB headsets, so fall
++ // back per field to sensible defaults.
++ constexpr int defaultSampleRate = 48000;
++ uint32_t *sampleRates = nullptr;
++ uint32_t sampleRateCount = 0;
++ if (OH_AudioDeviceDescriptor_GetDeviceSampleRates(descriptor, &sampleRates, &sampleRateCount)
++ == AUDIOCOMMON_RESULT_SUCCESS
++ && sampleRateCount > 0) {
++ const QSpan<const uint32_t> rates{ sampleRates, sampleRateCount };
++ format.setSampleRate(
++ QtMultimediaPrivate::findClosestSamplingRate(defaultSampleRate, rates));
++ } else {
++ format.setSampleRate(defaultSampleRate);
++ }
++
++ uint32_t *channelCounts = nullptr;
++ uint32_t channelCountSize = 0;
++ if (OH_AudioDeviceDescriptor_GetDeviceChannelCounts(descriptor, &channelCounts, &channelCountSize)
++ == AUDIOCOMMON_RESULT_SUCCESS
++ && channelCountSize > 0) {
++ const QSpan<const uint32_t> channels{ channelCounts, channelCountSize };
++ const uint32_t chosenChannels =
++ ranges::contains(channels, 2u) ? 2u : *ranges::max_element(channels);
++ format.setChannelConfig(
++ QAudioFormat::defaultChannelConfigForChannelCount(static_cast<int>(chosenChannels)));
++ info.maximumChannelCount = static_cast<int>(*ranges::max_element(channels));
++ } else {
++ format.setChannelConfig(QAudioFormat::ChannelConfigStereo);
++ }
++
++ format.setSampleFormat(QAudioFormat::Float);
++ return info;
++ }
++
++ QString deviceTypeLabel(OH_AudioDevice_Type type)
++ {
++ switch (type) {
++ case AUDIO_DEVICE_TYPE_EARPIECE:
++ return QStringLiteral("Earpiece");
++ case AUDIO_DEVICE_TYPE_SPEAKER:
++ return QStringLiteral("Speaker");
++ case AUDIO_DEVICE_TYPE_WIRED_HEADSET:
++ return QStringLiteral("Wired Headset");
++ case AUDIO_DEVICE_TYPE_WIRED_HEADPHONES:
++ return QStringLiteral("Wired Headphones");
++ case AUDIO_DEVICE_TYPE_BLUETOOTH_SCO:
++ return QStringLiteral("Bluetooth (SCO)");
++ case AUDIO_DEVICE_TYPE_BLUETOOTH_A2DP:
++ return QStringLiteral("Bluetooth (A2DP)");
++ case AUDIO_DEVICE_TYPE_MIC:
++ return QStringLiteral("Microphone");
++ case AUDIO_DEVICE_TYPE_USB_HEADSET:
++ case AUDIO_DEVICE_TYPE_USB_DEVICE:
++ return QStringLiteral("USB Audio");
++ default:
++ break;
++ }
++ return {};
++ }
++
++ QString deviceDisplayDescription(OH_AudioDeviceDescriptor *descriptor)
++ {
++ char *name = nullptr;
++ if (OH_AudioDeviceDescriptor_GetDeviceName(descriptor, &name) == AUDIOCOMMON_RESULT_SUCCESS
++ && name && *name) {
++ return QString::fromUtf8(name);
++ }
++
++ OH_AudioDevice_Type type{ AUDIO_DEVICE_TYPE_INVALID };
++ OH_AudioDeviceDescriptor_GetDeviceType(descriptor, &type);
++ const QString typeLabel = deviceTypeLabel(type);
++
++ QString displayName;
++ char *rawDisplayName = nullptr;
++ if (OH_AudioDeviceDescriptor_GetDeviceDisplayName(descriptor, &rawDisplayName)
++ == AUDIOCOMMON_RESULT_SUCCESS
++ && rawDisplayName && *rawDisplayName) {
++ displayName = QString::fromUtf8(rawDisplayName);
++ }
++
++ if (!displayName.isEmpty() && !typeLabel.isEmpty())
++ return displayName + u' ' + typeLabel;
++ if (!displayName.isEmpty())
++ return displayName;
++ if (!typeLabel.isEmpty())
++ return typeLabel;
++ return QStringLiteral("Audio Device");
++ }
++
++ std::optional<uint32_t> preferredDeviceId(OH_AudioRoutingManager *routing, QAudioDevice::Mode mode)
++ {
++ OH_AudioDeviceDescriptorArray *preferred = nullptr;
++ const OH_AudioCommon_Result result = (mode == QAudioDevice::Input)
++ ? OH_AudioRoutingManager_GetPreferredInputDevice(routing, AUDIOSTREAM_SOURCE_TYPE_MIC,
++ &preferred)
++ : OH_AudioRoutingManager_GetPreferredOutputDevice(routing, AUDIOSTREAM_USAGE_MUSIC,
++ &preferred);
++ if (result != AUDIOCOMMON_RESULT_SUCCESS || !preferred || preferred->size == 0) {
++ if (preferred)
++ OH_AudioRoutingManager_ReleaseDevices(routing, preferred);
++ return std::nullopt;
++ }
++
++ uint32_t deviceId = 0;
++ OH_AudioDeviceDescriptor_GetDeviceId(preferred->descriptors[0], &deviceId);
++ OH_AudioRoutingManager_ReleaseDevices(routing, preferred);
++ return deviceId;
++ }
++
++ QByteArray deviceIdentifier(OH_AudioDeviceDescriptor *descriptor)
++ {
++ // The numeric device id is reused after a device is unplugged, so prefer the
++ // stable device address as the identifier. Built-in devices report no
++ // address; fall back to their (stable) numeric id there.
++ char *address = nullptr;
++ if (OH_AudioDeviceDescriptor_GetDeviceAddress(descriptor, &address) == AUDIOCOMMON_RESULT_SUCCESS
++ && address && *address) {
++ return QByteArray{ address };
++ }
++ uint32_t deviceId = 0;
++ OH_AudioDeviceDescriptor_GetDeviceId(descriptor, &deviceId);
++ return QByteArray::number(deviceId);
++ }
++
++ QList<QAudioDevice> enumerateDevices(QAudioDevice::Mode mode)
++ {
++ OH_AudioManager *manager = nullptr;
++ if (OH_GetAudioManager(&manager) != AUDIOCOMMON_RESULT_SUCCESS || !manager) {
++ qCWarning(qLcOhosAudioDevices) << "OH_GetAudioManager failed";
++ return {};
++ }
++
++ OH_AudioRoutingManager *routing = nullptr;
++ if (OH_AudioManager_GetAudioRoutingManager(&routing) != AUDIOCOMMON_RESULT_SUCCESS || !routing) {
++ qCWarning(qLcOhosAudioDevices) << "OH_AudioManager_GetAudioRoutingManager failed";
++ return {};
++ }
++
++ const OH_AudioDevice_Flag flag = (mode == QAudioDevice::Input) ? AUDIO_DEVICE_FLAG_INPUT
++ : AUDIO_DEVICE_FLAG_OUTPUT;
++ OH_AudioDeviceDescriptorArray *descriptors = nullptr;
++ if (OH_AudioRoutingManager_GetDevices(routing, flag, &descriptors)
++ != AUDIOCOMMON_RESULT_SUCCESS
++ || !descriptors) {
++ qCWarning(qLcOhosAudioDevices) << "OH_AudioRoutingManager_GetDevices failed for"
++ << (mode == QAudioDevice::Input ? "input" : "output");
++ return {};
++ }
++
++ const std::optional<uint32_t> defaultDeviceId = preferredDeviceId(routing, mode);
++
++ QList<QAudioDevice> devices;
++ devices.reserve(descriptors->size);
++ for (uint32_t i = 0; i < descriptors->size; ++i) {
++ OH_AudioDeviceDescriptor *descriptor = descriptors->descriptors[i];
++ if (!descriptor)
++ continue;
++
++ uint32_t deviceId = 0;
++ OH_AudioDeviceDescriptor_GetDeviceId(descriptor, &deviceId);
++
++ const QByteArray identifier = deviceIdentifier(descriptor);
++ const DeviceFormatInfo formatInfo = deviceFormatInfo(descriptor);
++ const QString description = deviceDisplayDescription(descriptor);
++ const bool isDefault = defaultDeviceId ? deviceId == *defaultDeviceId : i == 0;
++
++ devices << QAudioDevicePrivate::createQAudioDevice(std::make_unique<QOhosAudioDevice>(
++ identifier, description, mode, formatInfo.preferredFormat, formatInfo.maximumChannelCount,
++ isDefault));
++ }
++
++ OH_AudioRoutingManager_ReleaseDevices(routing, descriptors);
++ return devices;
++ }
++
++ } // namespace
++
++ QOhosAudioDevices::QOhosAudioDevices() : QPlatformAudioDevices()
++ {
++ Q_ASSERT(!g_audioDevicesInstance);
++ g_audioDevicesInstance = this;
++ registerDeviceChangeCallbacks();
++ }
++
++ QOhosAudioDevices::~QOhosAudioDevices()
++ {
++ unregisterDeviceChangeCallbacks();
++ QMutexLocker guard{ &g_callbackMutex };
++ g_audioDevicesInstance = nullptr;
++ }
++
++ void QOhosAudioDevices::registerDeviceChangeCallbacks()
++ {
++ OH_AudioRoutingManager *routing = nullptr;
++ if (OH_AudioManager_GetAudioRoutingManager(&routing) != AUDIOCOMMON_RESULT_SUCCESS || !routing) {
++ qCWarning(qLcOhosAudioDevices)
++ << "Cannot register device change callbacks: routing manager unavailable";
++ return;
++ }
++
++ const OH_AudioCommon_Result inputResult = OH_AudioRoutingManager_RegisterDeviceChangeCallback(
++ routing, AUDIO_DEVICE_FLAG_INPUT, &QOhosAudioDevices::onInputDevicesChanged);
++ const OH_AudioCommon_Result outputResult = OH_AudioRoutingManager_RegisterDeviceChangeCallback(
++ routing, AUDIO_DEVICE_FLAG_OUTPUT, &QOhosAudioDevices::onOutputDevicesChanged);
++
++ if (inputResult != AUDIOCOMMON_RESULT_SUCCESS || outputResult != AUDIOCOMMON_RESULT_SUCCESS)
++ qCWarning(qLcOhosAudioDevices) << "Failed to register audio device change callbacks";
++
++ m_deviceChangeCallbacksRegistered =
++ inputResult == AUDIOCOMMON_RESULT_SUCCESS || outputResult == AUDIOCOMMON_RESULT_SUCCESS;
++ }
++
++ void QOhosAudioDevices::unregisterDeviceChangeCallbacks()
++ {
++ if (!m_deviceChangeCallbacksRegistered)
++ return;
++
++ OH_AudioRoutingManager *routing = nullptr;
++ if (OH_AudioManager_GetAudioRoutingManager(&routing) != AUDIOCOMMON_RESULT_SUCCESS || !routing)
++ return;
++
++ OH_AudioRoutingManager_UnregisterDeviceChangeCallback(
++ routing, &QOhosAudioDevices::onInputDevicesChanged);
++ OH_AudioRoutingManager_UnregisterDeviceChangeCallback(
++ routing, &QOhosAudioDevices::onOutputDevicesChanged);
++ }
++
++ // Invoked on an OHAudio service thread. We ignore the supplied descriptor array
++ // (owned by the framework) and just invalidate the cache so the next query
++ // re-enumerates.
++ int32_t QOhosAudioDevices::onInputDevicesChanged(OH_AudioDevice_ChangeType /*type*/,
++ OH_AudioDeviceDescriptorArray * /*devices*/)
++ {
++ QMutexLocker guard{ &g_callbackMutex };
++ if (g_audioDevicesInstance)
++ g_audioDevicesInstance->onAudioInputsChanged();
++ return 0;
++ }
++
++ int32_t QOhosAudioDevices::onOutputDevicesChanged(OH_AudioDevice_ChangeType /*type*/,
++ OH_AudioDeviceDescriptorArray * /*devices*/)
++ {
++ QMutexLocker guard{ &g_callbackMutex };
++ if (g_audioDevicesInstance)
++ g_audioDevicesInstance->onAudioOutputsChanged();
++ return 0;
++ }
++
++ QList<QAudioDevice> QOhosAudioDevices::findAudioInputs() const
++ {
++ return enumerateDevices(QAudioDevice::Input);
++ }
++
++ QList<QAudioDevice> QOhosAudioDevices::findAudioOutputs() const
++ {
++ return enumerateDevices(QAudioDevice::Output);
++ }
++
++ QPlatformAudioSource *QOhosAudioDevices::createAudioSource(const QAudioDevice &device,
++ const QAudioFormat &format,
++ QObject *parent)
++ {
++ return new QtOHAudio::QOhosAudioSource(device, format, parent);
++ }
++
++ QPlatformAudioSink *QOhosAudioDevices::createAudioSink(const QAudioDevice &device,
++ const QAudioFormat &format, QObject *parent)
++ {
++ return new QtOHAudio::QOhosAudioSink(device, format, parent);
++ }
++
++ QT_END_NAMESPACE