Allow audio sinks to match on device names

Names are more stable than IDs on Windows
This commit is contained in:
Cameron Gutman
2023-05-02 22:16:16 -05:00
parent 3fa5f74635
commit 1d6ea8c759
3 changed files with 109 additions and 14 deletions

View File

@@ -447,6 +447,8 @@ audio_sink
tools\audio-info.exe tools\audio-info.exe
.. Tip:: If you have multiple audio devices with identical names, use the Device ID instead.
.. Tip:: If you want to mute the host speakers, use `virtual_sink`_ instead. .. Tip:: If you want to mute the host speakers, use `virtual_sink`_ instead.
**Default** **Default**
@@ -466,7 +468,7 @@ audio_sink
**Windows** **Windows**
.. code-block:: text .. code-block:: text
audio_sink = {0.0.0.00000000}.{FD47D9CC-4218-4135-9CE2-0C195C87405B} audio_sink = Speakers (High Definition Audio Device)
virtual_sink virtual_sink
^^^^^^^^^^^^ ^^^^^^^^^^^^
@@ -488,7 +490,7 @@ virtual_sink
**Example** **Example**
.. code-block:: text .. code-block:: text
virtual_sink = {0.0.0.00000000}.{8edba70c-1125-467c-b89c-15da389bc1d4} virtual_sink = Steam Streaming Speakers
Network Network
------- -------

View File

@@ -515,7 +515,10 @@ namespace platf::audio {
UINT count; UINT count;
collection->GetCount(&count); collection->GetCount(&count);
std::string virtual_device_id = config::audio.virtual_sink; // If the sink isn't a device name, we'll assume it's a device ID
auto virtual_device_id = find_device_id_by_name(config::audio.virtual_sink).value_or(converter.from_bytes(config::audio.virtual_sink));
auto virtual_device_found = false;
for (auto x = 0; x < count; ++x) { for (auto x = 0; x < count; ++x) {
audio::device_t device; audio::device_t device;
collection->Item(x, &device); collection->Item(x, &device);
@@ -526,6 +529,7 @@ namespace platf::audio {
audio::wstring_t wstring; audio::wstring_t wstring;
device->GetId(&wstring); device->GetId(&wstring);
std::wstring device_id { wstring.get() };
audio::prop_t prop; audio::prop_t prop;
device->OpenPropertyStore(STGM_READ, &prop); device->OpenPropertyStore(STGM_READ, &prop);
@@ -548,17 +552,27 @@ namespace platf::audio {
<< std::endl; << std::endl;
if (virtual_device_id.empty() && adapter_name == virtual_adapter_name) { if (virtual_device_id.empty() && adapter_name == virtual_adapter_name) {
virtual_device_id = converter.to_bytes(wstring.get()); virtual_device_id = std::move(device_id);
virtual_device_found = true;
break;
}
else if (virtual_device_id == device_id) {
virtual_device_found = true;
break;
} }
} }
if (!virtual_device_id.empty()) { if (virtual_device_found) {
auto name_suffix = converter.to_bytes(virtual_device_id);
sink.null = std::make_optional(sink_t::null_t { sink.null = std::make_optional(sink_t::null_t {
"virtual-"s.append(formats[format_t::stereo - 1].name) + virtual_device_id, "virtual-"s.append(formats[format_t::stereo - 1].name) + name_suffix,
"virtual-"s.append(formats[format_t::surr51 - 1].name) + virtual_device_id, "virtual-"s.append(formats[format_t::surr51 - 1].name) + name_suffix,
"virtual-"s.append(formats[format_t::surr71 - 1].name) + virtual_device_id, "virtual-"s.append(formats[format_t::surr71 - 1].name) + name_suffix,
}); });
} }
else if (!virtual_device_id.empty()) {
BOOST_LOG(warning) << "Unable to find the specified virtual sink: "sv << virtual_device_id;
}
return sink; return sink;
} }
@@ -604,7 +618,8 @@ namespace platf::audio {
} }
} }
auto wstring_device_id = converter.from_bytes(sv.data()); // If the sink isn't a device name, we'll assume it's a device ID
auto wstring_device_id = find_device_id_by_name(sink).value_or(converter.from_bytes(sv.data()));
if (type == format_t::none) { if (type == format_t::none) {
// wstring_device_id does not contain virtual-(format name) // wstring_device_id does not contain virtual-(format name)
@@ -660,6 +675,83 @@ namespace platf::audio {
return failure; return failure;
} }
/**
* @brief Find the audio device ID given a user-specified name
*
* @param name The name provided by the user
*
* @return The matching device ID, or nothing if not found
*/
std::optional<std::wstring>
find_device_id_by_name(const std::string &name) {
if (name.empty()) {
return std::nullopt;
}
audio::device_enum_t device_enum;
auto status = CoCreateInstance(
CLSID_MMDeviceEnumerator,
nullptr,
CLSCTX_ALL,
IID_IMMDeviceEnumerator,
(void **) &device_enum);
if (FAILED(status)) {
BOOST_LOG(error) << "Couldn't create Device Enumerator: [0x"sv << util::hex(status).to_string_view() << ']';
return std::nullopt;
}
collection_t collection;
status = device_enum->EnumAudioEndpoints(eRender, DEVICE_STATE_ACTIVE, &collection);
if (FAILED(status)) {
BOOST_LOG(error) << "Couldn't enumerate: [0x"sv << util::hex(status).to_string_view() << ']';
return std::nullopt;
}
UINT count;
collection->GetCount(&count);
auto wstring_name = converter.from_bytes(name.data());
for (auto x = 0; x < count; ++x) {
audio::device_t device;
collection->Item(x, &device);
if (!validate_device(device)) {
continue;
}
audio::wstring_t wstring_id;
device->GetId(&wstring_id);
audio::prop_t prop;
device->OpenPropertyStore(STGM_READ, &prop);
prop_var_t adapter_friendly_name;
prop_var_t device_friendly_name;
prop_var_t device_desc;
prop->GetValue(PKEY_Device_FriendlyName, &device_friendly_name.prop);
prop->GetValue(PKEY_DeviceInterface_FriendlyName, &adapter_friendly_name.prop);
prop->GetValue(PKEY_Device_DeviceDesc, &device_desc.prop);
auto adapter_name = no_null((LPWSTR) adapter_friendly_name.prop.pszVal);
auto device_name = no_null((LPWSTR) device_friendly_name.prop.pszVal);
auto device_description = no_null((LPWSTR) device_desc.prop.pszVal);
// Match the user-specified name against any of the user-visible strings
if (std::wcscmp(wstring_name.c_str(), adapter_name) == 0 ||
std::wcscmp(wstring_name.c_str(), device_name) == 0 ||
std::wcscmp(wstring_name.c_str(), device_description) == 0) {
return std::make_optional(std::wstring { wstring_id.get() });
}
}
return std::nullopt;
}
int int
init() { init() {
auto status = CoCreateInstance( auto status = CoCreateInstance(

View File

@@ -448,13 +448,14 @@
type="text" type="text"
class="form-control" class="form-control"
id="audio_sink" id="audio_sink"
placeholder="{0.0.0.00000000}.{FD47D9CC-4218-4135-9CE2-0C195C87405B}" placeholder="Speakers (High Definition Audio Device)"
v-model="config.audio_sink" v-model="config.audio_sink"
/> />
<div class="form-text"> <div class="form-text">
The name of the audio sink used for Audio Loopback<br /> The name of the audio sink used for audio capture. If not set, the default audio device will be used.<br />
You can find the name of the audio sink using the following command:<br /> You can find the name of the audio sink using the following command:<br />
<pre>tools\audio-info.exe</pre> <pre>tools\audio-info.exe</pre>
If you have multiple audio devices with identical names, use the Device ID instead.
</div> </div>
</div> </div>
<div class="mb-3" v-if="platform === 'linux'"> <div class="mb-3" v-if="platform === 'linux'">
@@ -506,12 +507,12 @@
type="text" type="text"
class="form-control" class="form-control"
id="virtual_sink" id="virtual_sink"
placeholder="{0.0.0.00000000}.{8edba70c-1125-467c-b89c-15da389bc1d4}" placeholder="Steam Streaming Speakers"
v-model="config.virtual_sink" v-model="config.virtual_sink"
/> />
<div class="form-text"> <div class="form-text">
The virtual sink, is the audio device that's virtual (Like Steam Streaming Speakers), it allows Sunshine to The virtual sink is an audio device that's virtual (like Steam Streaming Speakers). It allows Sunshine to
stream audio, while muting the speakers. stream audio, while muting the host PC speakers.
</div> </div>
</div> </div>
<!--Adapter Name --> <!--Adapter Name -->