diff --git a/docs/source/about/advanced_usage.rst b/docs/source/about/advanced_usage.rst index 1353f4a1..c4f606cd 100644 --- a/docs/source/about/advanced_usage.rst +++ b/docs/source/about/advanced_usage.rst @@ -447,6 +447,8 @@ audio_sink 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. **Default** @@ -466,7 +468,7 @@ audio_sink **Windows** .. code-block:: text - audio_sink = {0.0.0.00000000}.{FD47D9CC-4218-4135-9CE2-0C195C87405B} + audio_sink = Speakers (High Definition Audio Device) virtual_sink ^^^^^^^^^^^^ @@ -488,7 +490,7 @@ virtual_sink **Example** .. code-block:: text - virtual_sink = {0.0.0.00000000}.{8edba70c-1125-467c-b89c-15da389bc1d4} + virtual_sink = Steam Streaming Speakers Network ------- diff --git a/src/platform/windows/audio.cpp b/src/platform/windows/audio.cpp index 9f38f5b6..cc31c0c5 100644 --- a/src/platform/windows/audio.cpp +++ b/src/platform/windows/audio.cpp @@ -515,7 +515,10 @@ namespace platf::audio { UINT 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) { audio::device_t device; collection->Item(x, &device); @@ -526,6 +529,7 @@ namespace platf::audio { audio::wstring_t wstring; device->GetId(&wstring); + std::wstring device_id { wstring.get() }; audio::prop_t prop; device->OpenPropertyStore(STGM_READ, &prop); @@ -548,17 +552,27 @@ namespace platf::audio { << std::endl; 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 { - "virtual-"s.append(formats[format_t::stereo - 1].name) + virtual_device_id, - "virtual-"s.append(formats[format_t::surr51 - 1].name) + virtual_device_id, - "virtual-"s.append(formats[format_t::surr71 - 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) + name_suffix, + "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; } @@ -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) { // wstring_device_id does not contain virtual-(format name) @@ -660,6 +675,83 @@ namespace platf::audio { 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 + 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 init() { auto status = CoCreateInstance( diff --git a/src_assets/common/assets/web/config.html b/src_assets/common/assets/web/config.html index 4754b4c2..84c90a53 100644 --- a/src_assets/common/assets/web/config.html +++ b/src_assets/common/assets/web/config.html @@ -448,13 +448,14 @@ type="text" class="form-control" id="audio_sink" - placeholder="{0.0.0.00000000}.{FD47D9CC-4218-4135-9CE2-0C195C87405B}" + placeholder="Speakers (High Definition Audio Device)" v-model="config.audio_sink" />
- The name of the audio sink used for Audio Loopback
+ The name of the audio sink used for audio capture. If not set, the default audio device will be used.
You can find the name of the audio sink using the following command:
tools\audio-info.exe
+ If you have multiple audio devices with identical names, use the Device ID instead.
@@ -506,12 +507,12 @@ type="text" class="form-control" id="virtual_sink" - placeholder="{0.0.0.00000000}.{8edba70c-1125-467c-b89c-15da389bc1d4}" + placeholder="Steam Streaming Speakers" v-model="config.virtual_sink" />
- The virtual sink, is the audio device that's virtual (Like Steam Streaming Speakers), it allows Sunshine to - stream audio, while muting the speakers. + The virtual sink is an audio device that's virtual (like Steam Streaming Speakers). It allows Sunshine to + stream audio, while muting the host PC speakers.