Skip to content
Snippets Groups Projects
Commit 95832cfc authored by Andreas Traczyk's avatar Andreas Traczyk Committed by Adrien Béraud
Browse files

audio: Windows: listen for device list change events

Modifies the Portaudio layer to listen for device list change
events and react accordingly.

Gitlab: #1120
Change-Id: Iba21ec5baf02fc8377923d6085e496e142ceac07
parent 4662710e
Branches
Tags
No related merge requests found
/*
* Copyright (C) 2025 Savoir-faire Linux Inc.
*
* 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 3 of the License, or
* (at your option) any later version.
*
* 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 <https://www.gnu.org/licenses/>.
*/
#include "string_utils.h"
#include "logger.h"
#include <atlbase.h>
#include <conio.h>
#include <mmdeviceapi.h>
#include <mmreg.h>
#include <string>
#include <windows.h>
#include <Functiondiscoverykeys_devpkey.h>
#include <functional>
#include <mutex>
#include <unordered_set>
// Note: the granularity of these events is not currently required because we end up
// restarting the audio layer regardless of the event type. However, let's keep it like
// this for now in case a future PortAudio bump or layer improvement can make use of it
// to avoid unnecessary stream restarts.
enum class DeviceEventType { BecameActive, BecameInactive, DefaultChanged, PropertyChanged };
inline std::string
to_string(DeviceEventType type)
{
switch (type) {
case DeviceEventType::BecameActive:
return "BecameActive";
case DeviceEventType::BecameInactive:
return "BecameInactive";
case DeviceEventType::DefaultChanged:
return "DefaultChanged";
case DeviceEventType::PropertyChanged:
return "PropertyChanged";
}
return "Unknown";
}
using DeviceEventCallback = std::function<void(const std::string& deviceName, DeviceEventType)>;
std::string
GetFriendlyNameFromIMMDeviceId(CComPtr<IMMDeviceEnumerator> enumerator, LPCWSTR deviceId)
{
if (!enumerator || !deviceId)
return {};
CComPtr<IMMDevice> device;
CComPtr<IPropertyStore> props;
if (FAILED(enumerator->GetDevice(deviceId, &device)))
return {};
if (FAILED(device->OpenPropertyStore(STGM_READ, &props)))
return {};
PROPVARIANT varName;
PropVariantInit(&varName);
if (SUCCEEDED(props->GetValue(PKEY_Device_FriendlyName, &varName))) {
std::wstring name = varName.pwszVal;
PropVariantClear(&varName);
return jami::to_string(name);
}
return {};
}
class AudioDeviceNotificationClient : public IMMNotificationClient
{
LONG _refCount;
public:
AudioDeviceNotificationClient()
: _refCount(1)
{
if (FAILED(CoCreateInstance(__uuidof(MMDeviceEnumerator),
nullptr,
CLSCTX_ALL,
__uuidof(IMMDeviceEnumerator),
(void**) &deviceEnumerator_))) {
JAMI_ERR("Failed to create device enumerator");
} else {
// Fill our list of active devices (render + capture)
enumerateDevices();
}
}
~AudioDeviceNotificationClient()
{
if (deviceEnumerator_) {
deviceEnumerator_->UnregisterEndpointNotificationCallback(this);
}
}
void setDeviceEventCallback(DeviceEventCallback callback)
{
if (!deviceEnumerator_) {
JAMI_ERR("Device enumerator not initialized");
return;
}
{
std::lock_guard<std::mutex> lock(mutex_);
deviceEventCallback_ = callback;
}
// Now we can start monitoring
deviceEnumerator_->RegisterEndpointNotificationCallback(this);
}
// // IUnknown methods
ULONG STDMETHODCALLTYPE AddRef() override
{
auto count = InterlockedIncrement(&_refCount);
return count;
}
ULONG STDMETHODCALLTYPE Release() override
{
auto count = InterlockedDecrement(&_refCount);
if (count == 0) {
delete this;
}
return count;
}
HRESULT STDMETHODCALLTYPE QueryInterface(REFIID riid, void** ppv) override
{
if (riid == __uuidof(IUnknown) || riid == __uuidof(IMMNotificationClient)) {
*ppv = static_cast<IMMNotificationClient*>(this);
AddRef();
return S_OK;
}
*ppv = nullptr;
return E_NOINTERFACE;
}
// IMMNotificationClient methods
HRESULT STDMETHODCALLTYPE OnDeviceStateChanged(LPCWSTR deviceId, DWORD newState) override
{
handleStateChanged(deviceId, newState);
return S_OK;
}
HRESULT STDMETHODCALLTYPE OnDeviceAdded(LPCWSTR deviceId) override
{
// Treat addition as transition to active - we'll validate the state in handler
handleStateChanged(deviceId, DEVICE_STATE_ACTIVE);
return S_OK;
}
HRESULT STDMETHODCALLTYPE OnDeviceRemoved(LPCWSTR deviceId) override
{
// Removed devices should be treated as inactive/unavailable
handleStateChanged(deviceId, DEVICE_STATE_NOTPRESENT);
return S_OK;
}
HRESULT STDMETHODCALLTYPE OnDefaultDeviceChanged(EDataFlow flow,
ERole role,
LPCWSTR deviceId) override
{
// If the default communication device changes, we should restart the layer
// to ensure the new device is used. We only care about the default communication
// device, so we ignore other roles.
if (role == eCommunications && deviceId && deviceEventCallback_) {
std::string friendlyName = GetFriendlyNameFromIMMDeviceId(deviceEnumerator_, deviceId);
deviceEventCallback_(friendlyName, DeviceEventType::DefaultChanged);
}
return S_OK;
}
HRESULT STDMETHODCALLTYPE OnPropertyValueChanged(LPCWSTR deviceId,
const PROPERTYKEY key) override
{
// Limit this to samplerate changes
if (key == PKEY_AudioEngine_DeviceFormat) {
// Fetch updated sample rate
IMMDevice* pDevice;
deviceEnumerator_->GetDevice(deviceId, &pDevice);
IPropertyStore* pProps;
pDevice->OpenPropertyStore(STGM_READ, &pProps);
PROPVARIANT var;
PropVariantInit(&var);
pProps->GetValue(PKEY_AudioEngine_DeviceFormat, &var);
if (var.vt == VT_BLOB) {
auto* pWaveFormat = reinterpret_cast<WAVEFORMATEXTENSIBLE*>(var.blob.pBlobData);
UINT sampleRate = pWaveFormat->Format.nSamplesPerSec;
JAMI_DBG("Sample rate changed to %u", sampleRate);
std::string friendlyName = GetFriendlyNameFromIMMDeviceId(deviceEnumerator_,
deviceId);
deviceEventCallback_(friendlyName, DeviceEventType::PropertyChanged);
}
PropVariantClear(&var);
pProps->Release();
pDevice->Release();
}
return S_OK;
}
private:
CComPtr<IMMDeviceEnumerator> deviceEnumerator_;
DeviceEventCallback deviceEventCallback_;
std::unordered_set<std::wstring> activeDevices_;
// Notifications are invoked on system-managed threads, so we need a lock
// to protect deviceEventCallback_ and activeDevices_
std::mutex mutex_;
// Enumerates both render (playback) and capture (recording) devices
void enumerateDevices()
{
if (!deviceEnumerator_)
return;
std::lock_guard<std::mutex> lock(mutex_);
activeDevices_.clear();
enumerateDevicesOfType(eRender);
enumerateDevicesOfType(eCapture);
}
void enumerateDevicesOfType(EDataFlow flow)
{
CComPtr<IMMDeviceCollection> devices;
CComPtr<IMMDevice> device;
UINT deviceCount = 0;
if (FAILED(deviceEnumerator_->EnumAudioEndpoints(flow, DEVICE_STATE_ACTIVE, &devices))) {
JAMI_ERR("Failed to enumerate devices");
return;
}
if (FAILED(devices->GetCount(&deviceCount))) {
JAMI_ERR("Failed to get device count");
return;
}
for (UINT i = 0; i < deviceCount; ++i) {
if (FAILED(devices->Item(i, &device)))
continue;
LPWSTR deviceId = nullptr;
if (FAILED(device->GetId(&deviceId)))
continue;
DWORD deviceState = 0;
if (SUCCEEDED(device->GetState(&deviceState)) && deviceState == DEVICE_STATE_ACTIVE) {
activeDevices_.insert(deviceId);
}
CoTaskMemFree(deviceId);
}
}
void handleStateChanged(LPCWSTR deviceId, DWORD newState)
{
if (!deviceId || !deviceEnumerator_ || !deviceEventCallback_)
return;
std::lock_guard<std::mutex> lock(mutex_);
std::wstring wsId(deviceId);
std::string friendlyName = GetFriendlyNameFromIMMDeviceId(deviceEnumerator_, deviceId);
// We only care if a device is entering or leaving the active state
bool isActive = (newState == DEVICE_STATE_ACTIVE);
auto it = activeDevices_.find(wsId);
if (isActive && it == activeDevices_.end()) {
// Device is active and not in our list? Add it and notify
activeDevices_.insert(wsId);
deviceEventCallback_(friendlyName, DeviceEventType::BecameActive);
} else if (!isActive && it != activeDevices_.end()) {
// Device is inactive and in our list? Remove it and notify
activeDevices_.erase(it);
deviceEventCallback_(friendlyName, DeviceEventType::BecameInactive);
}
}
};
// RAII COM wrapper for AudioDeviceNotificationClient
struct AudioDeviceNotificationClient_deleter
{
void operator()(AudioDeviceNotificationClient* client) const { client->Release(); }
};
typedef std::unique_ptr<AudioDeviceNotificationClient, AudioDeviceNotificationClient_deleter>
AudioDeviceNotificationClientPtr;
......@@ -16,16 +16,16 @@
*/
#include "portaudiolayer.h"
#include "manager.h"
#include "noncopyable.h"
#include "audio/resampler.h"
#include "audio/ringbufferpool.h"
#include "audio/ringbuffer.h"
#include "audio/audioloop.h"
#include "audio/portaudio/audio_device_monitor.h"
#include "manager.h"
#include "preferences.h"
#include <portaudio.h>
#include <algorithm>
#include <cmath>
#include <windows.h>
namespace jami {
......@@ -62,6 +62,11 @@ struct PortAudioLayer::PortAudioLayerImpl
std::array<PaStream*, static_cast<int>(Direction::End)> streams_;
AudioDeviceNotificationClientPtr audioDeviceNotificationClient_;
// The following flag used to debounce the device state changes,
// as default-device change events often follow plug/unplug events.
std::atomic<bool> restartRequestPending_ = false;
int paOutputCallback(PortAudioLayer& parent,
const int16_t* inputBuffer,
int16_t* outputBuffer,
......@@ -103,6 +108,9 @@ PortAudioLayer::PortAudioLayer(const AudioPreference& pref)
deviceInfo = Pa_GetDeviceInfo(i);
JAMI_DBG("PortAudio device: %d, %s", i, deviceInfo->name);
}
// Notify of device changes in case this layer was reset based on a hotplug event.
devicesChanged();
}
PortAudioLayer::~PortAudioLayer()
......@@ -298,7 +306,27 @@ PortAudioLayer::PortAudioLayerImpl::PortAudioLayerImpl(PortAudioLayer& parent,
: deviceRecord_ {pref.getPortAudioDeviceRecord()}
, devicePlayback_ {pref.getPortAudioDevicePlayback()}
, deviceRingtone_ {pref.getPortAudioDeviceRingtone()}
{
, audioDeviceNotificationClient_ {new AudioDeviceNotificationClient}
{
// Set up our callback to restart the layer on any device event
audioDeviceNotificationClient_->setDeviceEventCallback(
[this, &parent](const std::string& deviceName, const DeviceEventType event) {
JAMI_LOG("PortAudioLayer device event: {}, {}",
deviceName.c_str(),
to_string(event).c_str());
// Here we want to debounce the device events as a DefaultChanged could
// follow a DeviceAdded event and we don't want to restart the layer twice
if (!restartRequestPending_.exchange(true)) {
std::thread([] {
// First wait for the debounce period to pass, allowing for multiple events
// to be grouped together (e.g. DeviceAdded -> DefaultChanged).
std::this_thread::sleep_for(std::chrono::milliseconds(300));
auto currentAudioManager = Manager::instance().getAudioManager();
Manager::instance().setAudioPlugin(currentAudioManager);
}).detach();
}
});
init(parent);
}
......@@ -467,16 +495,15 @@ PaDeviceIndex
PortAudioLayer::PortAudioLayerImpl::getApiIndexByType(AudioDeviceType type)
{
auto numDevices = Pa_GetDeviceCount();
if (numDevices < 0)
if (numDevices < 0) {
JAMI_ERR("PortAudioLayer error: %s", Pa_GetErrorText(numDevices));
else {
return paNoDevice;
} else {
std::string_view toMatch = (type == AudioDeviceType::CAPTURE
? deviceRecord_
: (type == AudioDeviceType::PLAYBACK ? devicePlayback_
: deviceRingtone_));
if (toMatch.empty())
return type == AudioDeviceType::CAPTURE ? Pa_GetDefaultCommInputDevice()
: Pa_GetDefaultCommOutputDevice();
if (!toMatch.empty()) {
for (int i = 0; i < numDevices; ++i) {
if (const auto deviceInfo = Pa_GetDeviceInfo(i)) {
if (deviceInfo->name == toMatch)
......@@ -484,7 +511,10 @@ PortAudioLayer::PortAudioLayerImpl::getApiIndexByType(AudioDeviceType type)
}
}
}
return paNoDevice;
}
// If nothing was found, return the default device
return type == AudioDeviceType::CAPTURE ? Pa_GetDefaultCommInputDevice()
: Pa_GetDefaultCommOutputDevice();
}
std::string
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment