490 lines
16 KiB
C++
490 lines
16 KiB
C++
/**
|
|
* SPDX-FileCopyrightText: 2019 Weixuan XIAO <veyx.shaw@gmail.com>
|
|
*
|
|
* SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
|
|
*/
|
|
|
|
#include "systemvolumeplugin-macos.h"
|
|
|
|
#include <KPluginFactory>
|
|
|
|
#include <QDebug>
|
|
#include <QJsonArray>
|
|
#include <QJsonDocument>
|
|
#include <QJsonObject>
|
|
#include "plugin_systemvolume_debug.h"
|
|
|
|
K_PLUGIN_FACTORY_WITH_JSON( KdeConnectPluginFactory, "kdeconnect_systemvolume.json", registerPlugin< SystemvolumePlugin >(); )
|
|
|
|
|
|
class MacOSCoreAudioDevice
|
|
{
|
|
private:
|
|
AudioDeviceID m_deviceId;
|
|
QString m_description;
|
|
bool m_isStereo;
|
|
|
|
friend class SystemvolumePlugin;
|
|
public:
|
|
MacOSCoreAudioDevice(AudioDeviceID);
|
|
~MacOSCoreAudioDevice();
|
|
|
|
void setVolume(float volume);
|
|
float volume();
|
|
void setMuted(bool muted);
|
|
bool isMuted();
|
|
void setDefault(bool enabled);
|
|
bool isDefault();
|
|
|
|
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
|
|
};
|
|
|
|
static const AudioObjectPropertyAddress kAudioDefaultOutputDevicePropertyAddress = {
|
|
kAudioHardwarePropertyDefaultOutputDevice,
|
|
kAudioObjectPropertyScopeGlobal,
|
|
kAudioObjectPropertyElementMaster
|
|
};
|
|
|
|
OSStatus onVolumeChanged(AudioObjectID object, UInt32 numAddresses, const AudioObjectPropertyAddress addresses[], void *context)
|
|
{
|
|
Q_UNUSED(object);
|
|
Q_UNUSED(addresses);
|
|
Q_UNUSED(numAddresses);
|
|
|
|
SystemvolumePlugin *plugin = (SystemvolumePlugin*)context;
|
|
plugin->updateDeviceVolume(object);
|
|
return noErr;
|
|
}
|
|
|
|
OSStatus onMutedChanged(AudioObjectID object, UInt32 numAddresses, const AudioObjectPropertyAddress addresses[], void *context)
|
|
{
|
|
Q_UNUSED(object);
|
|
Q_UNUSED(addresses);
|
|
Q_UNUSED(numAddresses);
|
|
|
|
SystemvolumePlugin *plugin = (SystemvolumePlugin*)context;
|
|
plugin->updateDeviceMuted(object);
|
|
return noErr;
|
|
}
|
|
|
|
OSStatus onDefaultChanged(AudioObjectID object, UInt32 numAddresses, const AudioObjectPropertyAddress addresses[], void *context)
|
|
{
|
|
Q_UNUSED(object);
|
|
Q_UNUSED(addresses);
|
|
Q_UNUSED(numAddresses);
|
|
|
|
SystemvolumePlugin *plugin = (SystemvolumePlugin*)context;
|
|
plugin->sendSinkList();
|
|
return noErr;
|
|
}
|
|
|
|
OSStatus onOutputSourceChanged(AudioObjectID object, UInt32 numAddresses, const AudioObjectPropertyAddress addresses[], void *context)
|
|
{
|
|
Q_UNUSED(object);
|
|
Q_UNUSED(addresses);
|
|
Q_UNUSED(numAddresses);
|
|
|
|
SystemvolumePlugin *plugin = (SystemvolumePlugin*)context;
|
|
plugin->sendSinkList();
|
|
return noErr;
|
|
}
|
|
|
|
AudioObjectID getDefaultOutputDeviceId() {
|
|
AudioObjectID dataSourceId;
|
|
UInt32 size = sizeof(dataSourceId);
|
|
OSStatus result = AudioObjectGetPropertyData(kAudioObjectSystemObject, &kAudioDefaultOutputDevicePropertyAddress, 0, NULL, &size, &dataSourceId);
|
|
if (result != noErr)
|
|
return kAudioDeviceUnknown;
|
|
|
|
return dataSourceId;
|
|
}
|
|
|
|
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<AudioObjectID> GetAllOutputAudioDeviceIDs() {
|
|
std::vector<AudioObjectID> 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 deviceCount = size / sizeof(AudioObjectID);
|
|
std::vector<AudioObjectID> deviceIds(deviceCount);
|
|
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), m_sinksMap()
|
|
{}
|
|
|
|
SystemvolumePlugin::~SystemvolumePlugin()
|
|
{
|
|
AudioObjectRemovePropertyListener(kAudioObjectSystemObject, &kAudioDefaultOutputDevicePropertyAddress, &onDefaultChanged, (void *)this);
|
|
}
|
|
|
|
bool SystemvolumePlugin::receivePacket(const NetworkPacket& np)
|
|
{
|
|
if (np.has(QStringLiteral("requestSinks"))) {
|
|
sendSinkList();
|
|
} else {
|
|
QString name = np.get<QString>(QStringLiteral("name"));
|
|
|
|
if (m_sinksMap.contains(name)) {
|
|
if (np.has(QStringLiteral("volume"))) {
|
|
m_sinksMap[name]->setVolume(np.get<int>(QStringLiteral("volume")) / 100.0);
|
|
}
|
|
if (np.has(QStringLiteral("muted"))) {
|
|
m_sinksMap[name]->setMuted(np.get<bool>(QStringLiteral("muted")));
|
|
}
|
|
if (np.has(QStringLiteral("enabled"))) {
|
|
m_sinksMap[name]->setDefault(np.get<bool>(QStringLiteral("enabled")));
|
|
}
|
|
}
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
void SystemvolumePlugin::sendSinkList()
|
|
{
|
|
QJsonDocument document;
|
|
QJsonArray array;
|
|
|
|
if (!m_sinksMap.empty()) {
|
|
for (MacOSCoreAudioDevice *sink : m_sinksMap) {
|
|
delete sink;
|
|
}
|
|
m_sinksMap.clear();
|
|
}
|
|
|
|
std::vector<AudioObjectID> deviceIds = GetAllOutputAudioDeviceIDs();
|
|
|
|
for (AudioDeviceID deviceId : deviceIds) {
|
|
MacOSCoreAudioDevice *audioDevice = new MacOSCoreAudioDevice(deviceId);
|
|
|
|
audioDevice->m_description = translateDeviceSource(deviceId);
|
|
|
|
m_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->m_description},
|
|
{QStringLiteral("volume"), audioDevice->volume() * 100},
|
|
{QStringLiteral("maxVolume"), 100},
|
|
{QStringLiteral("enabled"), audioDevice->isDefault()}
|
|
};
|
|
|
|
array.append(sinkObject);
|
|
}
|
|
|
|
document.setArray(array);
|
|
|
|
NetworkPacket np(PACKET_TYPE_SYSTEMVOLUME);
|
|
np.set<QJsonDocument>(QStringLiteral("sinkList"), document);
|
|
sendPacket(np);
|
|
}
|
|
|
|
void SystemvolumePlugin::connected()
|
|
{
|
|
AudioObjectAddPropertyListener(kAudioObjectSystemObject, &kAudioDefaultOutputDevicePropertyAddress, &onDefaultChanged, (void *)this);
|
|
sendSinkList();
|
|
}
|
|
|
|
void SystemvolumePlugin::updateDeviceMuted(AudioDeviceID deviceId)
|
|
{
|
|
for (MacOSCoreAudioDevice *sink : m_sinksMap) {
|
|
if (sink->m_deviceId == deviceId) {
|
|
NetworkPacket np(PACKET_TYPE_SYSTEMVOLUME);
|
|
np.set<bool>(QStringLiteral("muted"), (bool)(sink->isMuted()));
|
|
np.set<int>(QStringLiteral("volume"), (int)(sink->volume() * 100));
|
|
np.set<QString>(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 : m_sinksMap) {
|
|
if (sink->m_deviceId == deviceId) {
|
|
NetworkPacket np(PACKET_TYPE_SYSTEMVOLUME);
|
|
np.set<int>(QStringLiteral("volume"), (int)(sink->volume() * 100));
|
|
np.set<QString>(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)
|
|
: m_deviceId(deviceId)
|
|
{
|
|
updateType();
|
|
}
|
|
|
|
MacOSCoreAudioDevice::~MacOSCoreAudioDevice()
|
|
{
|
|
// Volume listener
|
|
AudioObjectRemovePropertyListener(m_deviceId, &kAudioMasterVolumePropertyAddress,
|
|
&onVolumeChanged, (void *)this);
|
|
AudioObjectRemovePropertyListener(m_deviceId, &kAudioLeftVolumePropertyAddress,
|
|
&onVolumeChanged, (void *)this);
|
|
AudioObjectRemovePropertyListener(m_deviceId, &kAudioRightVolumePropertyAddress,
|
|
&onVolumeChanged, (void *)this);
|
|
|
|
// Muted listener
|
|
AudioObjectRemovePropertyListener(m_deviceId, &kAudioMasterMutedPropertyAddress,
|
|
&onMutedChanged, (void *)this);
|
|
|
|
// Data source listener
|
|
AudioObjectRemovePropertyListener(m_deviceId, &kAudioMasterDataSourcePropertyAddress,
|
|
&onOutputSourceChanged, (void *)this);
|
|
}
|
|
|
|
void MacOSCoreAudioDevice::setVolume(float volume)
|
|
{
|
|
OSStatus result;
|
|
|
|
if (m_deviceId == kAudioObjectUnknown) {
|
|
qWarning(KDECONNECT_PLUGIN_SYSTEMVOLUME) << "Unable to set volume of Unknown Device";
|
|
return;
|
|
}
|
|
|
|
if (m_isStereo) {
|
|
result = AudioObjectSetPropertyData(m_deviceId, &kAudioLeftVolumePropertyAddress, 0, NULL, sizeof(volume), &volume);
|
|
result = AudioObjectSetPropertyData(m_deviceId, &kAudioRightVolumePropertyAddress, 0, NULL, sizeof(volume), &volume);
|
|
} else {
|
|
result = AudioObjectSetPropertyData(m_deviceId, &kAudioMasterVolumePropertyAddress, 0, NULL, sizeof(volume), &volume);
|
|
}
|
|
|
|
if (result != noErr) {
|
|
qWarning(KDECONNECT_PLUGIN_SYSTEMVOLUME) << "Unable to set volume of Device" << m_deviceId << "to" << volume;
|
|
}
|
|
}
|
|
|
|
void MacOSCoreAudioDevice::setMuted(bool muted)
|
|
{
|
|
if (m_deviceId == kAudioObjectUnknown) {
|
|
qWarning(KDECONNECT_PLUGIN_SYSTEMVOLUME) << "Unable to mute an Unknown Device";
|
|
return;
|
|
}
|
|
|
|
UInt32 mutedValue = muted ? 1 : 0;
|
|
|
|
OSStatus result = AudioObjectSetPropertyData(m_deviceId, &kAudioMasterMutedPropertyAddress, 0, NULL, sizeof(mutedValue), &mutedValue);
|
|
|
|
if (result != noErr) {
|
|
qWarning(KDECONNECT_PLUGIN_SYSTEMVOLUME) << "Unable to set muted state of Device" << m_deviceId << "to" << muted;
|
|
}
|
|
}
|
|
|
|
void MacOSCoreAudioDevice::setDefault(bool enabled)
|
|
{
|
|
if (!enabled) return;
|
|
|
|
if (m_deviceId == kAudioObjectUnknown) {
|
|
qWarning(KDECONNECT_PLUGIN_SYSTEMVOLUME) << "Unable to set an Unknown Device as default output";
|
|
return;
|
|
}
|
|
|
|
OSStatus result = AudioObjectSetPropertyData(kAudioObjectSystemObject, &kAudioDefaultOutputDevicePropertyAddress, 0, NULL, sizeof(m_deviceId), &m_deviceId);
|
|
|
|
if (result != noErr) {
|
|
qWarning(KDECONNECT_PLUGIN_SYSTEMVOLUME) << "Unable to set default state of Device" << m_deviceId;
|
|
}
|
|
}
|
|
|
|
float MacOSCoreAudioDevice::volume()
|
|
{
|
|
OSStatus result;
|
|
|
|
if (m_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 (m_isStereo) {
|
|
// Try to get steoreo device volume
|
|
result = AudioObjectGetPropertyData(m_deviceId, &kAudioLeftVolumePropertyAddress, 0, NULL, &volumeDataSize, &volume);
|
|
} else {
|
|
// Try to get master volume
|
|
result = AudioObjectGetPropertyData(m_deviceId, &kAudioMasterVolumePropertyAddress, 0, NULL, &volumeDataSize, &volume);
|
|
}
|
|
|
|
if (result != noErr) {
|
|
qWarning(KDECONNECT_PLUGIN_SYSTEMVOLUME) << "Unable to get volume of Device" << m_deviceId;
|
|
return 0.0;
|
|
}
|
|
|
|
return volume;
|
|
}
|
|
|
|
bool MacOSCoreAudioDevice::isMuted()
|
|
{
|
|
if (m_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(m_deviceId, &kAudioMasterMutedPropertyAddress, 0, NULL, &muteddataSize, &muted);
|
|
|
|
return muted == 1;
|
|
}
|
|
|
|
bool MacOSCoreAudioDevice::isDefault() {
|
|
AudioObjectID defaultDeviceId = getDefaultOutputDeviceId();
|
|
return m_deviceId == defaultDeviceId;
|
|
}
|
|
|
|
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(m_deviceId, &kAudioLeftVolumePropertyAddress, 0, NULL, &volumeDataSize, &volume);
|
|
if (result == noErr) {
|
|
m_isStereo = true;
|
|
} else {
|
|
m_isStereo = false;
|
|
}
|
|
}
|
|
|
|
|
|
#include "systemvolumeplugin-macos.moc"
|
|
|