/** * Copyright 2019 Weixuan XIAO * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU General Public License as * published by the Free Software Foundation; either version 2 of * the License or (at your option) version 3 or any later version * accepted by the membership of KDE e.V. (or its successor approved * by the membership of KDE e.V.), which shall act as a proxy * defined in Section 14 of version 3 of the license. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ #include "systemvolumeplugin-macos.h" #include #include #include #include #include #include K_PLUGIN_FACTORY_WITH_JSON( KdeConnectPluginFactory, "kdeconnect_systemvolume.json", registerPlugin< SystemvolumePlugin >(); ) Q_LOGGING_CATEGORY(KDECONNECT_PLUGIN_SYSTEMVOLUME, "kdeconnect.plugin.systemvolume") class MacOSCoreAudioDevice { private: AudioDeviceID deviceId; QString description; bool isStereo; friend class SystemvolumePlugin; public: MacOSCoreAudioDevice(AudioDeviceID); ~MacOSCoreAudioDevice(); void setVolume(float volume); float volume(); void setMuted(bool muted); bool isMuted(); void updateType(); }; static const AudioObjectPropertyAddress kAudioHardwarePropertyAddress = { kAudioHardwarePropertyDevices, kAudioObjectPropertyScopeGlobal, kAudioObjectPropertyElementMaster}; static const AudioObjectPropertyAddress kAudioStreamPropertyAddress = { kAudioDevicePropertyStreams, kAudioDevicePropertyScopeOutput, kAudioObjectPropertyElementMaster}; static const AudioObjectPropertyAddress kAudioMasterVolumePropertyAddress = { kAudioDevicePropertyVolumeScalar, kAudioDevicePropertyScopeOutput, kAudioObjectPropertyElementMaster}; static const AudioObjectPropertyAddress kAudioLeftVolumePropertyAddress = { kAudioDevicePropertyVolumeScalar, kAudioDevicePropertyScopeOutput, 1}; static const AudioObjectPropertyAddress kAudioRightVolumePropertyAddress = { kAudioDevicePropertyVolumeScalar, kAudioDevicePropertyScopeOutput, 2}; static const AudioObjectPropertyAddress kAudioMasterMutedPropertyAddress = { kAudioDevicePropertyMute, kAudioDevicePropertyScopeOutput, kAudioObjectPropertyElementMaster}; static const AudioObjectPropertyAddress kAudioMasterDataSourcePropertyAddress = { kAudioDevicePropertyDataSource, kAudioDevicePropertyScopeOutput, kAudioObjectPropertyElementMaster}; OSStatus onVolumeChanged(AudioObjectID object, UInt32 num_addresses, const AudioObjectPropertyAddress addresses[], void *context) { SystemvolumePlugin *plugin = (SystemvolumePlugin*)context; plugin->updateDeviceVolume(object); return noErr; } OSStatus onMutedChanged(AudioObjectID object, UInt32 num_addresses, const AudioObjectPropertyAddress addresses[], void *context) { SystemvolumePlugin *plugin = (SystemvolumePlugin*)context; plugin->updateDeviceMuted(object); return noErr; } OSStatus onOutputSourceChanged(AudioObjectID object, UInt32 num_addresses, const AudioObjectPropertyAddress addresses[], void *context) { SystemvolumePlugin *plugin = (SystemvolumePlugin*)context; plugin->sendSinkList(); return noErr; } UInt32 getDeviceSourceId(AudioObjectID deviceId) { UInt32 dataSourceId; UInt32 size = sizeof(dataSourceId); OSStatus result = AudioObjectGetPropertyData(deviceId, &kAudioMasterDataSourcePropertyAddress, 0, NULL, &size, &dataSourceId); if (result != noErr) return kAudioDeviceUnknown; return dataSourceId; } QString translateDeviceSource(AudioObjectID deviceId) { UInt32 sourceId = getDeviceSourceId(deviceId); if (sourceId == kAudioDeviceUnknown) { qWarning(KDECONNECT_PLUGIN_SYSTEMVOLUME) << "Unknown data source id of device" << deviceId; return QStringLiteral(""); } CFStringRef sourceName = nullptr; AudioValueTranslation translation; translation.mInputData = &sourceId; translation.mInputDataSize = sizeof(sourceId); translation.mOutputData = &sourceName; translation.mOutputDataSize = sizeof(sourceName); UInt32 translationSize = sizeof(AudioValueTranslation); AudioObjectPropertyAddress propertyAddress = { kAudioDevicePropertyDataSourceNameForIDCFString, kAudioDevicePropertyScopeOutput, kAudioObjectPropertyElementMaster}; OSStatus result = AudioObjectGetPropertyData(deviceId, &propertyAddress, 0, NULL, &translationSize, &translation); if (result != noErr) { qWarning(KDECONNECT_PLUGIN_SYSTEMVOLUME) << "Cannot get description of device" << deviceId; return QStringLiteral(""); } QString ret = QString::fromCFString(sourceName); CFRelease(sourceName); return ret; } std::vector GetAllOutputAudioDeviceIDs() { std::vector outputDeviceIds; UInt32 size = 0; OSStatus result; result = AudioObjectGetPropertyDataSize(kAudioObjectSystemObject, &kAudioHardwarePropertyAddress, 0, NULL, &size); if (result != noErr) { qCDebug(KDECONNECT_PLUGIN_SYSTEMVOLUME) << "Failed to read size of property " << kAudioHardwarePropertyDevices << " for device/object " << kAudioObjectSystemObject; return {}; } if (size == 0) return {}; size_t device_count = size / sizeof(AudioObjectID); std::vector deviceIds(device_count); result = AudioObjectGetPropertyData(kAudioObjectSystemObject, &kAudioHardwarePropertyAddress, 0, NULL, &size, deviceIds.data()); if (result != noErr) { qCDebug(KDECONNECT_PLUGIN_SYSTEMVOLUME) << "Failed to read object IDs from property " << kAudioHardwarePropertyDevices << " for device/object " << kAudioObjectSystemObject; return {}; } for (AudioDeviceID deviceId : deviceIds) { UInt32 streamCount = 0; result = AudioObjectGetPropertyDataSize(deviceId, &kAudioStreamPropertyAddress, 0, NULL, &streamCount); if (result == noErr && streamCount > 0) { outputDeviceIds.push_back(deviceId); qCDebug(KDECONNECT_PLUGIN_SYSTEMVOLUME) << "Device" << deviceId << "added"; } else { qCDebug(KDECONNECT_PLUGIN_SYSTEMVOLUME) << "Device" << deviceId << "dropped"; } } return outputDeviceIds; } SystemvolumePlugin::SystemvolumePlugin(QObject* parent, const QVariantList& args) : KdeConnectPlugin(parent, args) , sinksMap() {} bool SystemvolumePlugin::receivePacket(const NetworkPacket& np) { if (np.has(QStringLiteral("requestSinks"))) { sendSinkList(); } else { QString name = np.get(QStringLiteral("name")); if (sinksMap.contains(name)) { if (np.has(QStringLiteral("volume"))) { sinksMap[name]->setVolume(np.get(QStringLiteral("volume")) / 100.0); } if (np.has(QStringLiteral("muted"))) { sinksMap[name]->setMuted(np.get(QStringLiteral("muted"))); } } } return true; } void SystemvolumePlugin::sendSinkList() { QJsonDocument document; QJsonArray array; if (!sinksMap.empty()) { for (MacOSCoreAudioDevice *sink : sinksMap) { delete sink; } sinksMap.clear(); } std::vector deviceIds = GetAllOutputAudioDeviceIDs(); for (AudioDeviceID deviceId : deviceIds) { MacOSCoreAudioDevice *audioDevice = new MacOSCoreAudioDevice(deviceId); audioDevice->description = translateDeviceSource(deviceId); sinksMap.insert(QStringLiteral("default-") + QString::number(deviceId), audioDevice); // Add volume change listener AudioObjectAddPropertyListener(deviceId, &kAudioMasterVolumePropertyAddress, &onVolumeChanged, (void *)this); AudioObjectAddPropertyListener(deviceId, &kAudioLeftVolumePropertyAddress, &onVolumeChanged, (void *)this); AudioObjectAddPropertyListener(deviceId, &kAudioRightVolumePropertyAddress, &onVolumeChanged, (void *)this); // Add muted change listener AudioObjectAddPropertyListener(deviceId, &kAudioMasterMutedPropertyAddress, &onMutedChanged, (void *)this); // Add data source change listerner AudioObjectAddPropertyListener(deviceId, &kAudioMasterDataSourcePropertyAddress, &onOutputSourceChanged, (void *)this); QJsonObject sinkObject { {QStringLiteral("name"), QStringLiteral("default-") + QString::number(deviceId)}, {QStringLiteral("muted"), audioDevice->isMuted()}, {QStringLiteral("description"), audioDevice->description}, {QStringLiteral("volume"), audioDevice->volume() * 100}, {QStringLiteral("maxVolume"), 100} }; array.append(sinkObject); } document.setArray(array); NetworkPacket np(PACKET_TYPE_SYSTEMVOLUME); np.set(QStringLiteral("sinkList"), document); sendPacket(np); } void SystemvolumePlugin::connected() { sendSinkList(); } void SystemvolumePlugin::updateDeviceMuted(AudioDeviceID deviceId) { for (MacOSCoreAudioDevice *sink : sinksMap) { if (sink->deviceId == deviceId) { NetworkPacket np(PACKET_TYPE_SYSTEMVOLUME); np.set(QStringLiteral("muted"), (bool)(sink->isMuted())); np.set(QStringLiteral("volume"), (int)(sink->volume() * 100)); np.set(QStringLiteral("name"), QStringLiteral("default-") + QString::number(deviceId)); sendPacket(np); return; } } qCDebug(KDECONNECT_PLUGIN_SYSTEMVOLUME) << "Device" << deviceId << "not found while update mute state"; } void SystemvolumePlugin::updateDeviceVolume(AudioDeviceID deviceId) { for (MacOSCoreAudioDevice *sink : sinksMap) { if (sink->deviceId == deviceId) { NetworkPacket np(PACKET_TYPE_SYSTEMVOLUME); np.set(QStringLiteral("volume"), (int)(sink->volume() * 100)); np.set(QStringLiteral("name"), QStringLiteral("default-") + QString::number(deviceId)); sendPacket(np); return; } } qCDebug(KDECONNECT_PLUGIN_SYSTEMVOLUME) << "Device" << deviceId << "not found while update volume"; } MacOSCoreAudioDevice::MacOSCoreAudioDevice(AudioDeviceID _deviceId) : deviceId(_deviceId) { updateType(); } MacOSCoreAudioDevice::~MacOSCoreAudioDevice() { // Volume listener AudioObjectRemovePropertyListener(deviceId, &kAudioMasterVolumePropertyAddress, &onVolumeChanged, (void *)this); AudioObjectRemovePropertyListener(deviceId, &kAudioLeftVolumePropertyAddress, &onVolumeChanged, (void *)this); AudioObjectRemovePropertyListener(deviceId, &kAudioRightVolumePropertyAddress, &onVolumeChanged, (void *)this); // Muted listener AudioObjectRemovePropertyListener(deviceId, &kAudioMasterMutedPropertyAddress, &onMutedChanged, (void *)this); // Data source listener AudioObjectRemovePropertyListener(deviceId, &kAudioMasterDataSourcePropertyAddress, &onOutputSourceChanged, (void *)this); } void MacOSCoreAudioDevice::setVolume(float volume) { OSStatus result; if (deviceId == kAudioObjectUnknown) { qWarning(KDECONNECT_PLUGIN_SYSTEMVOLUME) << "Unable to set volume of Unknown Device"; return; } if (isStereo) { result = AudioObjectSetPropertyData(deviceId, &kAudioLeftVolumePropertyAddress, 0, NULL, sizeof(volume), &volume); result = AudioObjectSetPropertyData(deviceId, &kAudioRightVolumePropertyAddress, 0, NULL, sizeof(volume), &volume); } else { result = AudioObjectSetPropertyData(deviceId, &kAudioMasterVolumePropertyAddress, 0, NULL, sizeof(volume), &volume); } if (result != noErr) { qWarning(KDECONNECT_PLUGIN_SYSTEMVOLUME) << "Unable to set volume of Device" << deviceId << "to" << volume; } } void MacOSCoreAudioDevice::setMuted(bool muted) { if (deviceId == kAudioObjectUnknown) { qWarning(KDECONNECT_PLUGIN_SYSTEMVOLUME) << "Unable to mute an Unknown Device"; return; } UInt32 mutedValue = muted ? 1 : 0; OSStatus result = AudioObjectSetPropertyData(deviceId, &kAudioMasterMutedPropertyAddress, 0, NULL, sizeof(mutedValue), &mutedValue); if (result != noErr) { qWarning(KDECONNECT_PLUGIN_SYSTEMVOLUME) << "Unable to set muted state of Device" << deviceId << "to" << muted; } } float MacOSCoreAudioDevice::volume() { OSStatus result; if (deviceId == kAudioObjectUnknown) { qWarning(KDECONNECT_PLUGIN_SYSTEMVOLUME) << "Unable to get volume of Unknown Device"; return 0.0; } float volume = 0.0; UInt32 volumeDataSize = sizeof(volume); if (isStereo) { // Try to get steoreo device volume result = AudioObjectGetPropertyData(deviceId, &kAudioLeftVolumePropertyAddress, 0, NULL, &volumeDataSize, &volume); } else { // Try to get master volume result = AudioObjectGetPropertyData(deviceId, &kAudioMasterVolumePropertyAddress, 0, NULL, &volumeDataSize, &volume); } if (result != noErr) { qWarning(KDECONNECT_PLUGIN_SYSTEMVOLUME) << "Unable to get volume of Device" << deviceId; return 0.0; } return volume; } bool MacOSCoreAudioDevice::isMuted() { if (deviceId == kAudioObjectUnknown) { qWarning(KDECONNECT_PLUGIN_SYSTEMVOLUME) << "Unable to get muted state of an Unknown Device"; return false; } UInt32 muted = 0; UInt32 muteddataSize = sizeof(muted); AudioObjectGetPropertyData(deviceId, &kAudioMasterMutedPropertyAddress, 0, NULL, &muteddataSize, &muted); return muted == 1; } void MacOSCoreAudioDevice::updateType() { // Try to get volume from left channel to check if it's a stereo device float volume = 0.0; UInt32 volumeDataSize = sizeof(volume); OSStatus result = AudioObjectGetPropertyData(deviceId, &kAudioLeftVolumePropertyAddress, 0, NULL, &volumeDataSize, &volume); if (result == noErr) { isStereo = true; } else { isStereo = false; } } #include "systemvolumeplugin-macos.moc"