Please disclose if any significant portion of your mod was created using AI tools by adding the 'AI Generated' category. Failing to do so may result in the mod being removed from Thunderstore.
Decompiled source of Custom Audio Output Device v1.0.2
plugins/Soundheim.dll
Decompiled a week agousing System; using System.Collections.Generic; using System.ComponentModel; using System.Diagnostics; using System.Globalization; using System.IO; using System.Linq; using System.Reflection; using System.Runtime.CompilerServices; using System.Runtime.InteropServices; using System.Runtime.Versioning; using System.Threading.Tasks; using BepInEx; using BepInEx.Configuration; using GUIFramework; using HarmonyLib; using Microsoft.CodeAnalysis; using Newtonsoft.Json; using Newtonsoft.Json.Linq; using Soundheim.Audio; using TMPro; using UnityEngine; using UnityEngine.Events; using UnityEngine.UI; using Valheim.SettingsGui; [assembly: CompilationRelaxations(8)] [assembly: RuntimeCompatibility(WrapNonExceptionThrows = true)] [assembly: Debuggable(DebuggableAttribute.DebuggingModes.IgnoreSymbolStoreSequencePoints)] [assembly: AssemblyTitle("Custom Audio Output Device")] [assembly: AssemblyProduct("Custom Audio Output Device")] [assembly: AssemblyFileVersion("1.0.2.0")] [assembly: TargetFramework(".NETStandard,Version=v2.1", FrameworkDisplayName = ".NET Standard 2.1")] [assembly: AssemblyVersion("1.0.2.0")] [module: RefSafetyRules(11)] namespace Microsoft.CodeAnalysis { [CompilerGenerated] [Embedded] internal sealed class EmbeddedAttribute : Attribute { } } namespace System.Runtime.CompilerServices { [CompilerGenerated] [Embedded] [AttributeUsage(AttributeTargets.Class | AttributeTargets.Property | AttributeTargets.Field | AttributeTargets.Event | AttributeTargets.Parameter | AttributeTargets.ReturnValue | AttributeTargets.GenericParameter, AllowMultiple = false, Inherited = false)] internal sealed class NullableAttribute : Attribute { public readonly byte[] NullableFlags; public NullableAttribute(byte P_0) { NullableFlags = new byte[1] { P_0 }; } public NullableAttribute(byte[] P_0) { NullableFlags = P_0; } } [CompilerGenerated] [Embedded] [AttributeUsage(AttributeTargets.Class | AttributeTargets.Struct | AttributeTargets.Method | AttributeTargets.Interface | AttributeTargets.Delegate, AllowMultiple = false, Inherited = false)] internal sealed class NullableContextAttribute : Attribute { public readonly byte Flag; public NullableContextAttribute(byte P_0) { Flag = P_0; } } [CompilerGenerated] [Embedded] [AttributeUsage(AttributeTargets.Module, AllowMultiple = false, Inherited = false)] internal sealed class RefSafetyRulesAttribute : Attribute { public readonly int Version; public RefSafetyRulesAttribute(int P_0) { Version = P_0; } } } namespace Soundheim { internal sealed class AudioDeviceTooltips(GameObject prefab, UITooltip selected) { internal static AudioDeviceTooltips? Create(TMP_Dropdown dropdown, Transform parent) { //IL_002a: Unknown result type (might be due to invalid IL or missing references) //IL_0030: Expected O, but got Unknown //IL_0083: Unknown result type (might be due to invalid IL or missing references) //IL_0088: Unknown result type (might be due to invalid IL or missing references) //IL_009a: Unknown result type (might be due to invalid IL or missing references) //IL_00b4: Unknown result type (might be due to invalid IL or missing references) //IL_00bb: Unknown result type (might be due to invalid IL or missing references) //IL_00cd: Unknown result type (might be due to invalid IL or missing references) //IL_00e2: Unknown result type (might be due to invalid IL or missing references) //IL_00f7: Unknown result type (might be due to invalid IL or missing references) //IL_0101: Unknown result type (might be due to invalid IL or missing references) //IL_011c: Unknown result type (might be due to invalid IL or missing references) //IL_012c: Unknown result type (might be due to invalid IL or missing references) //IL_013a: Unknown result type (might be due to invalid IL or missing references) //IL_0144: Expected O, but got Unknown //IL_0182: Unknown result type (might be due to invalid IL or missing references) //IL_019a: Unknown result type (might be due to invalid IL or missing references) TMP_Text captionText = dropdown.captionText; if ((Object)(object)captionText == (Object)null) { return null; } GameObject val = new GameObject("Audio device tooltip template", new Type[1] { typeof(RectTransform) }); val.SetActive(false); val.transform.SetParent(parent, false); GameObject val2 = new GameObject("Panel", new Type[4] { typeof(RectTransform), typeof(Image), typeof(VerticalLayoutGroup), typeof(ContentSizeFitter) }); val2.transform.SetParent(val.transform, false); RectTransform component = val2.GetComponent<RectTransform>(); Vector2 val3 = default(Vector2); ((Vector2)(ref val3))..ctor(0.5f, 0.5f); component.anchorMax = val3; component.anchorMin = val3; component.pivot = new Vector2(0f, 1f); component.anchoredPosition = new Vector2(16f, -16f); component.sizeDelta = new Vector2(400f, 0f); Image component2 = val2.GetComponent<Image>(); ((Graphic)component2).color = new Color(0f, 0f, 0f, 0.9f); ((Graphic)component2).raycastTarget = false; VerticalLayoutGroup component3 = val2.GetComponent<VerticalLayoutGroup>(); ((LayoutGroup)component3).padding = new RectOffset(12, 12, 8, 8); ((HorizontalOrVerticalLayoutGroup)component3).spacing = 4f; bool childControlWidth = (((HorizontalOrVerticalLayoutGroup)component3).childControlHeight = true); ((HorizontalOrVerticalLayoutGroup)component3).childControlWidth = childControlWidth; ((HorizontalOrVerticalLayoutGroup)component3).childForceExpandWidth = true; ((HorizontalOrVerticalLayoutGroup)component3).childForceExpandHeight = false; val2.GetComponent<ContentSizeFitter>().verticalFit = (FitMode)2; TMP_Text val4 = CreateLabel((Transform)(object)component, captionText, "Topic", ((Graphic)captionText).color, (TextAlignmentOptions)514); TMP_Text val5 = CreateLabel((Transform)(object)component, captionText, "Text", Color.white, (TextAlignmentOptions)513); val.AddComponent<AudioTooltipSize>().Initialize(component, component3, val4, val5); UITooltip selected = Attach(((Component)dropdown).gameObject, val, captionText.text); return new AudioDeviceTooltips(val, selected); } private static TMP_Text CreateLabel(Transform parent, TMP_Text style, string name, Color color, TextAlignmentOptions alignment) { //IL_0014: Unknown result type (might be due to invalid IL or missing references) //IL_0019: Unknown result type (might be due to invalid IL or missing references) //IL_0050: Unknown result type (might be due to invalid IL or missing references) //IL_0057: Unknown result type (might be due to invalid IL or missing references) GameObject val = new GameObject(name, new Type[1] { typeof(RectTransform) }); val.transform.SetParent(parent, false); TextMeshProUGUI obj = val.AddComponent<TextMeshProUGUI>(); ((TMP_Text)obj).font = style.font; ((TMP_Text)obj).fontSharedMaterial = style.fontSharedMaterial; ((TMP_Text)obj).fontSize = style.fontSizeMax; ((Graphic)obj).color = color; ((TMP_Text)obj).alignment = alignment; ((TMP_Text)obj).richText = false; ((Graphic)obj).raycastTarget = false; ((TMP_Text)obj).textWrappingMode = (TextWrappingModes)1; return (TMP_Text)(object)obj; } internal void SetSelection(string name) { //IL_001c: Unknown result type (might be due to invalid IL or missing references) //IL_0022: Unknown result type (might be due to invalid IL or missing references) if (((Behaviour)selected).enabled) { selected.Set("Output device", name, (RectTransform)null, default(Vector2)); } else { selected.m_text = name; } } internal void SetExpanded(TMP_Dropdown dropdown, bool expanded) { ((Behaviour)selected).enabled = !expanded; if (!expanded) { return; } Toggle[] componentsInChildren = ((Component)dropdown).GetComponentsInChildren<Toggle>(); foreach (Toggle val in componentsInChildren) { TMP_Text componentInChildren = ((Component)val).GetComponentInChildren<TMP_Text>(); if ((Object)(object)componentInChildren != (Object)null) { Attach(((Component)val).gameObject, prefab, componentInChildren.text); } } } private static UITooltip Attach(GameObject target, GameObject prefab, string name) { UITooltip obj = target.GetComponent<UITooltip>() ?? target.AddComponent<UITooltip>(); obj.m_tooltipPrefab = prefab; obj.m_topic = "Output device"; obj.m_text = name; return obj; } } internal sealed class AudioTooltipSize : MonoBehaviour { [SerializeField] private RectTransform? panel; [SerializeField] private VerticalLayoutGroup? layout; [SerializeField] private TMP_Text[] labels = Array.Empty<TMP_Text>(); internal void Initialize(RectTransform panel, VerticalLayoutGroup layout, params TMP_Text[] labels) { this.panel = panel; this.layout = layout; this.labels = labels; } private void OnEnable() { //IL_0041: Unknown result type (might be due to invalid IL or missing references) if (!((Object)(object)panel == (Object)null) && !((Object)(object)layout == (Object)null)) { float num = 0f; TMP_Text[] array = labels; foreach (TMP_Text obj in array) { float x = obj.GetPreferredValues(obj.text, float.PositiveInfinity, float.PositiveInfinity).x; num = Mathf.Max(num, x); } num = Mathf.Min(400f, Mathf.Ceil(num) + (float)((LayoutGroup)layout).padding.horizontal); panel.SetSizeWithCurrentAnchors((Axis)0, num); LayoutRebuilder.ForceRebuildLayoutImmediate(panel); } } } internal sealed class NativeAudioSettings : MonoBehaviour { [HarmonyPatch(typeof(AudioSettings), "Initialize")] private static class InitializePatch { private static void Postfix(AudioSettings __instance, Toggle ___m_continousMusic, Slider ___m_volumeSlider, TMP_Text ___m_volumeText) { Attach(__instance, ___m_continousMusic, ___m_volumeSlider, ___m_volumeText); } } [HarmonyPatch(typeof(AudioSettings), "OnTabOpen")] private static class NavigationPatch { private static void Postfix(AudioSettings __instance, Toggle ___m_continousMusic, Button backButton, Button okButton) { ((Component)__instance).GetComponent<NativeAudioSettings>()?.SetNavigation(___m_continousMusic, backButton, okButton); } } [HarmonyPatch(typeof(AudioSettings), "OnOkAsync")] private static class SavePatch { private static void Prefix() { Plugin.Instance?.Save(); } } [HarmonyPatch(typeof(AudioSettings), "OnBack")] private static class RevertPatch { private static void Postfix() { Plugin.Instance?.Revert(); } } private GuiDropdown? dropdown; private TMP_Text? status; private AudioDeviceTooltips? tooltips; private readonly List<string> identifiers = new List<string>(); private IReadOnlyList<AudioDevice>? shownDevices; private string shownSelection = ""; private Settings? settings; private Plugin? plugin; private RectTransform? rowRect; private RectTransform? parentRect; private RectTransform? previousRect; private RectTransform? sliderRect; private RectTransform? volumeRect; private RectTransform? labelRect; private readonly Vector3[] corners = (Vector3[])(object)new Vector3[4]; internal static void Attach(AudioSettings audio, Toggle lastControl, Slider volume, TMP_Text volumeText) { if ((Object)(object)Plugin.Instance == (Object)null || (Object)(object)((Component)audio).GetComponent<NativeAudioSettings>() != (Object)null) { return; } Settings componentInParent = ((Component)audio).GetComponentInParent<Settings>(); GraphicsSettings componentInChildren = ((Component)componentInParent).GetComponentInChildren<GraphicsSettings>(true); object obj2; if ((Object)(object)componentInChildren != (Object)null) { object? obj = AccessTools.Field(typeof(GraphicsSettings), "m_resolutionDropdown")?.GetValue(componentInChildren); GuiDropdown val = (GuiDropdown)((obj is GuiDropdown) ? obj : null); if (val != null) { obj2 = val; goto IL_0062; } } obj2 = null; goto IL_0062; IL_0062: GuiDropdown val2 = (GuiDropdown)obj2; TMP_Text componentInChildren2 = ((Component)lastControl).GetComponentInChildren<TMP_Text>(true); if ((Object)(object)val2 == (Object)null || (Object)(object)componentInChildren2 == (Object)null) { Plugin.Instance.ReportError("Custom Audio Output Device could not find Valheim's native audio label or dropdown. Check compatibility with this Valheim version."); return; } NativeAudioSettings nativeAudioSettings = ((Component)audio).gameObject.AddComponent<NativeAudioSettings>(); nativeAudioSettings.settings = componentInParent; nativeAudioSettings.plugin = Plugin.Instance; nativeAudioSettings.sliderRect = ((Component)volume).GetComponent<RectTransform>(); nativeAudioSettings.volumeRect = volumeText.rectTransform; nativeAudioSettings.Create(audio, lastControl, val2, componentInChildren2); Plugin.Instance.Refresh(); } private void Create(AudioSettings audio, Toggle lastControl, GuiDropdown template, TMP_Text font) { //IL_002c: Unknown result type (might be due to invalid IL or missing references) //IL_0032: Expected O, but got Unknown //IL_005d: Unknown result type (might be due to invalid IL or missing references) //IL_0072: Unknown result type (might be due to invalid IL or missing references) //IL_0087: Unknown result type (might be due to invalid IL or missing references) //IL_00bc: Unknown result type (might be due to invalid IL or missing references) //IL_014d: Unknown result type (might be due to invalid IL or missing references) //IL_0158: Unknown result type (might be due to invalid IL or missing references) //IL_016d: Unknown result type (might be due to invalid IL or missing references) //IL_0178: Unknown result type (might be due to invalid IL or missing references) //IL_018c: Unknown result type (might be due to invalid IL or missing references) //IL_019c: Unknown result type (might be due to invalid IL or missing references) //IL_01a6: Expected O, but got Unknown //IL_01ae: Unknown result type (might be due to invalid IL or missing references) //IL_01bc: Unknown result type (might be due to invalid IL or missing references) RectTransform component = ((Component)audio).GetComponent<RectTransform>(); GameObject val = new GameObject("Soundheim output device", new Type[2] { typeof(RectTransform), typeof(LayoutElement) }); val.transform.SetParent((Transform)(object)component, false); val.GetComponent<LayoutElement>().ignoreLayout = true; RectTransform component2 = val.GetComponent<RectTransform>(); component2.anchorMin = new Vector2(0f, 1f); component2.anchorMax = new Vector2(1f, 1f); component2.pivot = new Vector2(0.5f, 1f); rowRect = component2; parentRect = component; previousRect = ((Component)lastControl).GetComponent<RectTransform>(); PositionRow(); component2.sizeDelta = new Vector2(-48f, 112f); TMP_Text val2 = Label(val.transform, font, "Output device", 0f, 0f, 0.42f, 40f); val2.alignment = (TextAlignmentOptions)4100; labelRect = val2.rectTransform; dropdown = Object.Instantiate<GuiDropdown>(template, val.transform); ((Object)dropdown).name = "Output device"; ((Component)dropdown).gameObject.SetActive(false); RectTransform component3 = ((Component)dropdown).GetComponent<RectTransform>(); component3.anchorMin = new Vector2(0.44f, 1f); component3.anchorMax = Vector2.one; component3.pivot = new Vector2(0.5f, 1f); component3.anchoredPosition = Vector2.zero; component3.sizeDelta = new Vector2(0f, 40f); ((TMP_Dropdown)dropdown).onValueChanged = new DropdownEvent(); GuiDropdown? obj = dropdown; Navigation navigation = default(Navigation); ((Navigation)(ref navigation)).mode = (Mode)4; ((Selectable)obj).navigation = navigation; ((TMP_Dropdown)dropdown).ClearOptions(); ((UnityEvent<int>)(object)((TMP_Dropdown)dropdown).onValueChanged).AddListener((UnityAction<int>)Selected); dropdown.OnExpandedStateChange += Expanded; TMP_Text[] componentsInChildren = ((Component)dropdown).GetComponentsInChildren<TMP_Text>(true); foreach (TMP_Text val3 in componentsInChildren) { if ((Object)(object)val3 != (Object)(object)((TMP_Dropdown)dropdown).captionText && (Object)(object)val3 != (Object)(object)((TMP_Dropdown)dropdown).itemText && ((Object)(object)((TMP_Dropdown)dropdown).template == (Object)null || !val3.transform.IsChildOf((Transform)(object)((TMP_Dropdown)dropdown).template))) { ((Behaviour)val3).enabled = false; } } ConfigureDeviceText(((TMP_Dropdown)dropdown).captionText); ConfigureDeviceText(((TMP_Dropdown)dropdown).itemText); status = Label(val.transform, font, "", 0f, 48f, 1f, 64f); status.fontSize = Math.Min(font.fontSize, 16f); status.enableAutoSizing = false; status.alignment = (TextAlignmentOptions)257; status.textWrappingMode = (TextWrappingModes)1; status.overflowMode = (TextOverflowModes)1; tooltips = AudioDeviceTooltips.Create((TMP_Dropdown)(object)dropdown, val.transform); if (tooltips == null) { Plugin.Instance?.ReportTooltipUnavailable(); } RefreshOptions(); ((Component)dropdown).gameObject.SetActive(true); PositionRow(); } private static void ConfigureDeviceText(TMP_Text? text) { //IL_004f: Unknown result type (might be due to invalid IL or missing references) //IL_005a: Unknown result type (might be due to invalid IL or missing references) //IL_006f: Unknown result type (might be due to invalid IL or missing references) //IL_0083: Unknown result type (might be due to invalid IL or missing references) if (!((Object)(object)text == (Object)null)) { text.richText = false; text.textWrappingMode = (TextWrappingModes)0; text.overflowMode = (TextOverflowModes)1; text.enableAutoSizing = true; text.fontSizeMax = text.fontSize; text.fontSizeMin = Math.Min(16f, text.fontSizeMax); RectTransform rectTransform = text.rectTransform; rectTransform.anchorMin = Vector2.zero; rectTransform.anchorMax = Vector2.one; rectTransform.offsetMin = new Vector2(16f, 0f); rectTransform.offsetMax = new Vector2(-16f, 0f); } } private static TMP_Text Label(Transform parent, TMP_Text style, string text, float x, float y, float width, float height) { //IL_0024: Unknown result type (might be due to invalid IL or missing references) //IL_002a: Expected O, but got Unknown //IL_0063: Unknown result type (might be due to invalid IL or missing references) //IL_006f: Unknown result type (might be due to invalid IL or missing references) //IL_00aa: Unknown result type (might be due to invalid IL or missing references) //IL_00bc: Unknown result type (might be due to invalid IL or missing references) //IL_00d1: Unknown result type (might be due to invalid IL or missing references) //IL_00e0: Unknown result type (might be due to invalid IL or missing references) //IL_00ee: Unknown result type (might be due to invalid IL or missing references) GameObject val = new GameObject((text.Length > 0) ? text : "Audio output status", new Type[1] { typeof(RectTransform) }); val.transform.SetParent(parent, false); TextMeshProUGUI obj = val.AddComponent<TextMeshProUGUI>(); ((TMP_Text)obj).font = style.font; ((TMP_Text)obj).fontSharedMaterial = style.fontSharedMaterial; ((TMP_Text)obj).fontSize = style.fontSize; ((TMP_Text)obj).fontStyle = style.fontStyle; ((Graphic)obj).color = ((Graphic)style).color; ((TMP_Text)obj).richText = false; ((Graphic)obj).raycastTarget = false; ((TMP_Text)obj).alignment = (TextAlignmentOptions)4097; ((TMP_Text)obj).text = text; RectTransform component = val.GetComponent<RectTransform>(); component.anchorMin = new Vector2(0f, 1f); component.anchorMax = new Vector2(width, 1f); component.pivot = new Vector2(0f, 1f); component.anchoredPosition = new Vector2(x, 0f - y); component.sizeDelta = new Vector2(0f - x, height); return (TMP_Text)(object)obj; } private void Selected(int index) { if (index >= 0 && index < identifiers.Count) { plugin?.Preview(identifiers[index]); } if ((Object)(object)dropdown != (Object)null && index >= 0 && index < ((TMP_Dropdown)dropdown).options.Count) { tooltips?.SetSelection(((TMP_Dropdown)dropdown).options[index].text); } } private void Expanded(bool expanded) { if ((Object)(object)settings != (Object)null) { settings.BlockNavigation(expanded); } if ((Object)(object)status != (Object)null) { ((Behaviour)status).enabled = !expanded; } if ((Object)(object)dropdown != (Object)null) { tooltips?.SetExpanded((TMP_Dropdown)(object)dropdown, expanded); } } private void LateUpdate() { PositionRow(); } private void PositionRow() { //IL_0049: Unknown result type (might be due to invalid IL or missing references) //IL_004e: Unknown result type (might be due to invalid IL or missing references) //IL_005e: Unknown result type (might be due to invalid IL or missing references) //IL_0063: Unknown result type (might be due to invalid IL or missing references) //IL_007f: Unknown result type (might be due to invalid IL or missing references) //IL_00e0: Unknown result type (might be due to invalid IL or missing references) //IL_00e5: Unknown result type (might be due to invalid IL or missing references) //IL_00f5: Unknown result type (might be due to invalid IL or missing references) //IL_00fa: Unknown result type (might be due to invalid IL or missing references) //IL_0122: Unknown result type (might be due to invalid IL or missing references) //IL_0127: Unknown result type (might be due to invalid IL or missing references) //IL_0137: Unknown result type (might be due to invalid IL or missing references) //IL_013c: Unknown result type (might be due to invalid IL or missing references) //IL_0163: Unknown result type (might be due to invalid IL or missing references) //IL_016a: Unknown result type (might be due to invalid IL or missing references) //IL_0181: Unknown result type (might be due to invalid IL or missing references) //IL_0191: Unknown result type (might be due to invalid IL or missing references) //IL_01b7: Unknown result type (might be due to invalid IL or missing references) //IL_01df: Unknown result type (might be due to invalid IL or missing references) //IL_01e6: Unknown result type (might be due to invalid IL or missing references) //IL_01f8: Unknown result type (might be due to invalid IL or missing references) //IL_0209: Unknown result type (might be due to invalid IL or missing references) //IL_0225: Unknown result type (might be due to invalid IL or missing references) //IL_0264: Unknown result type (might be due to invalid IL or missing references) //IL_026b: Unknown result type (might be due to invalid IL or missing references) //IL_0283: Unknown result type (might be due to invalid IL or missing references) //IL_02aa: Unknown result type (might be due to invalid IL or missing references) if ((Object)(object)rowRect == (Object)null || (Object)(object)parentRect == (Object)null || (Object)(object)previousRect == (Object)null) { return; } previousRect.GetWorldCorners(corners); float y = ((Transform)parentRect).InverseTransformPoint(corners[0]).y; Rect rect = parentRect.rect; float num = y - ((Rect)(ref rect)).yMax - 20f; rowRect.anchoredPosition = new Vector2(0f, num); if (!((Object)(object)sliderRect == (Object)null) && !((Object)(object)volumeRect == (Object)null) && !((Object)(object)labelRect == (Object)null) && !((Object)(object)dropdown == (Object)null)) { sliderRect.GetWorldCorners(corners); float x = ((Transform)rowRect).InverseTransformPoint(corners[0]).x; rect = rowRect.rect; float num2 = x - ((Rect)(ref rect)).xMin; volumeRect.GetWorldCorners(corners); float x2 = ((Transform)rowRect).InverseTransformPoint(corners[2]).x; rect = rowRect.rect; float num3 = x2 - ((Rect)(ref rect)).xMin; RectTransform? obj = labelRect; RectTransform? obj2 = labelRect; Vector2 val = default(Vector2); ((Vector2)(ref val))..ctor(0f, 1f); obj2.anchorMax = val; obj.anchorMin = val; labelRect.pivot = new Vector2(0f, 1f); labelRect.anchoredPosition = Vector2.zero; labelRect.sizeDelta = new Vector2(Math.Max(0f, num2 - 16f), 40f); RectTransform component = ((Component)dropdown).GetComponent<RectTransform>(); ((Vector2)(ref val))..ctor(0f, 1f); component.anchorMax = val; component.anchorMin = val; component.pivot = new Vector2(0f, 1f); component.anchoredPosition = new Vector2(num2, 0f); component.sizeDelta = new Vector2(Math.Max(200f, num3 - num2), 40f); if ((Object)(object)status != (Object)null) { RectTransform rectTransform = status.rectTransform; RectTransform rectTransform2 = status.rectTransform; ((Vector2)(ref val))..ctor(0f, 1f); rectTransform2.anchorMax = val; rectTransform.anchorMin = val; status.rectTransform.anchoredPosition = new Vector2(num2, -48f); status.rectTransform.sizeDelta = new Vector2(Math.Max(200f, num3 - num2), 64f); } } } private void Update() { if (!((Object)(object)plugin == (Object)null) && !((Object)(object)dropdown == (Object)null)) { if (!((TMP_Dropdown)dropdown).IsExpanded && (shownDevices != plugin.Devices || shownSelection != plugin.Selection)) { RefreshOptions(); } if ((Object)(object)status != (Object)null) { status.text = plugin.Status; } } } private void RefreshOptions() { if ((Object)(object)plugin == (Object)null || (Object)(object)dropdown == (Object)null) { return; } shownDevices = plugin.Devices; shownSelection = plugin.Selection; identifiers.Clear(); identifiers.Add(""); List<string> list = new List<string> { "System default" }; foreach (AudioDevice device in plugin.Devices) { identifiers.Add(device.Id); list.Add(device.Name); } int num = identifiers.IndexOf(plugin.Selection); if (num < 0) { num = identifiers.Count; identifiers.Add(plugin.Selection); list.Add("Selected device (disconnected)"); } ((TMP_Dropdown)dropdown).ClearOptions(); ((TMP_Dropdown)dropdown).AddOptions(list); ((TMP_Dropdown)dropdown).SetValueWithoutNotify(num); ((TMP_Dropdown)dropdown).RefreshShownValue(); tooltips?.SetSelection(list[num]); } internal void SetNavigation(Toggle previous, Button back, Button ok) { if (!((Object)(object)dropdown == (Object)null)) { GuiUtils.SetNavigationDown((Selectable)(object)previous, (Selectable)(object)dropdown); GuiUtils.SetNavigationUp((Selectable)(object)dropdown, (Selectable)(object)previous); GuiUtils.SetNavigationDown((Selectable)(object)dropdown, (Selectable)(object)back); GuiUtils.SetNavigationUp((Selectable)(object)back, (Selectable)(object)dropdown); GuiUtils.SetNavigationUp((Selectable)(object)ok, (Selectable)(object)dropdown); } } private void OnEnable() { plugin?.Refresh(); } private void OnDisable() { if ((Object)(object)dropdown != (Object)null && ((TMP_Dropdown)dropdown).IsExpanded) { ((TMP_Dropdown)dropdown).Hide(); Expanded(expanded: false); } } private void OnDestroy() { if ((Object)(object)dropdown != (Object)null) { dropdown.OnExpandedStateChange -= Expanded; } plugin?.Revert(); } } [BepInPlugin("augusdogus.mods.Soundheim", "Custom Audio Output Device", "1.0.2")] public sealed class Plugin : BaseUnityPlugin { private sealed class PollResult(int revision, AudioResult<IReadOnlyList<AudioDevice>> devices, AudioResult<string> route) { public int Revision { get; } = revision; public AudioResult<IReadOnlyList<AudioDevice>> Devices { get; } = devices; public AudioResult<string> Route { get; } = route; } public const string PluginId = "augusdogus.mods.Soundheim"; public const string PluginName = "Custom Audio Output Device"; public const string PluginVersion = "1.0.2"; private ConfigEntry<string>? preference; private IAudioBackend? backend; private Task<PollResult>? pending; private float nextPoll; private int revision; private string lastError = ""; private readonly Harmony harmony = new Harmony("augusdogus.mods.Soundheim"); internal static Plugin? Instance { get; private set; } internal IReadOnlyList<AudioDevice> Devices { get; private set; } = Array.Empty<AudioDevice>(); internal string Selection { get; private set; } = ""; internal string Status { get; private set; } = "Reading audio outputs..."; private void Awake() { //IL_0000: Unknown result type (might be due to invalid IL or missing references) //IL_0006: Invalid comparison between Unknown and I4 //IL_0016: Unknown result type (might be due to invalid IL or missing references) //IL_001d: Invalid comparison between Unknown and I4 //IL_0042: Unknown result type (might be due to invalid IL or missing references) //IL_0048: Invalid comparison between Unknown and I4 if ((int)SystemInfo.graphicsDeviceType == 4) { ((Behaviour)this).enabled = false; return; } using Process process = Process.GetCurrentProcess(); if ((int)Application.platform == 13) { backend = new PulseAudioBackend(process.Id, new Pactl().Run); } else { if ((int)Application.platform != 2) { ((BaseUnityPlugin)this).Logger.LogWarning((object)"Custom Audio Output Device supports Linux and Windows only."); ((Behaviour)this).enabled = false; return; } backend = new WindowsAudioBackend(process.Id); } preference = ((BaseUnityPlugin)this).Config.Bind<string>("Audio", "Output device", "", "Stable output identifier. Empty follows the system default. Select a device in Settings > Audio."); Selection = preference.Value; preference.SettingChanged += PreferenceChanged; Instance = this; harmony.PatchAll(typeof(NativeAudioSettings).Assembly); ((BaseUnityPlugin)this).Logger.LogInfo((object)"Custom Audio Output Device loaded. Select your output in Settings > Audio."); } internal void Preview(string id) { Selection = id; revision++; nextPoll = 0f; Status = "Applying output preference..."; } internal void Save() { if (preference != null) { preference.Value = Selection; } } internal void Revert() { if (preference != null && Selection != preference.Value) { Preview(preference.Value); } } internal void Refresh() { nextPoll = 0f; } internal void ReportTooltipUnavailable() { ((BaseUnityPlugin)this).Logger.LogWarning((object)"Audio device tooltips could not find the dropdown's text style. Device selection remains available."); } private void PreferenceChanged(object sender, EventArgs args) { if (preference != null) { Preview(preference.Value); } } private void Update() { if (backend == null) { return; } if (pending != null && pending.IsCompleted) { Task<PollResult> task = pending; pending = null; if (task.IsFaulted) { ReportError(task.Exception?.GetBaseException().Message ?? "Unknown audio error."); } else { PollResult result = task.GetAwaiter().GetResult(); if (result.Devices is AudioResult<IReadOnlyList<AudioDevice>>.Success success) { Devices = success.Value; } else if (result.Devices is AudioResult<IReadOnlyList<AudioDevice>>.Failure failure) { ReportError(failure.Message); } if (result.Revision == revision) { if (result.Route is AudioResult<string>.Success success2) { Status = success2.Value; lastError = ""; } else if (result.Route is AudioResult<string>.Failure failure2) { ReportError(failure2.Message); } } } } if (pending == null && !(Time.unscaledTime < nextPoll)) { nextPoll = Time.unscaledTime + 5f; string selected = Selection; int requestRevision = revision; IAudioBackend selectedBackend = backend; pending = Task.Run(() => new PollResult(requestRevision, selectedBackend.ListOutputs(), selectedBackend.Route(selected))); } } internal void ReportError(string message) { Status = "Couldn't update audio output. Retrying automatically. Details are in the game log."; if (!(message == lastError)) { lastError = message; ((BaseUnityPlugin)this).Logger.LogWarning((object)message); } } private void OnDestroy() { if (preference != null) { preference.SettingChanged -= PreferenceChanged; } harmony.UnpatchSelf(); if ((Object)(object)Instance == (Object)(object)this) { Instance = null; } } } } namespace Soundheim.Audio { internal sealed class AudioDevice(string id, string name) { public string Id { get; } = id; public string Name { get; } = name; } internal abstract class AudioResult<T> { internal sealed class Success(T value) : AudioResult<T>() { public T Value { get; } = value; } internal sealed class Failure(string message) : AudioResult<T>() { public string Message { get; } = message; } private AudioResult() { } } internal interface IAudioBackend { AudioResult<IReadOnlyList<AudioDevice>> ListOutputs(); AudioResult<string> Route(string deviceId); } internal static class AudioBoundary { public static AudioResult<T> Run<T>(Func<T> action) { try { return new AudioResult<T>.Success(action()); } catch (Exception ex) when (ex is IOException || ex is UnauthorizedAccessException || ex is Win32Exception || ex is InvalidOperationException || ex is ArgumentException || ex is TimeoutException || ex is FormatException || ex is JsonException || ex is ExternalException || ex is NotSupportedException || ex is DllNotFoundException || ex is EntryPointNotFoundException) { return new AudioResult<T>.Failure(ex.Message); } } } internal sealed class Pactl { private const string SteamLauncher = "/usr/bin/steam-runtime-launch-client"; private static ProcessStartInfo StartInfo(string[] arguments, bool host) { ProcessStartInfo processStartInfo = new ProcessStartInfo(host ? "/usr/bin/steam-runtime-launch-client" : "pactl") { UseShellExecute = false, RedirectStandardOutput = true, RedirectStandardError = true, CreateNoWindow = true }; string[] array; if (host) { array = new string[4] { "--alongside-steam", "--env=LC_ALL=C", "--", "pactl" }; foreach (string item in array) { processStartInfo.ArgumentList.Add(item); } } array = arguments; foreach (string item2 in array) { processStartInfo.ArgumentList.Add(item2); } processStartInfo.Environment["LC_ALL"] = "C"; processStartInfo.Environment.Remove("LD_PRELOAD"); return processStartInfo; } public string Run(params string[] arguments) { using Process process = new Process { StartInfo = StartInfo(arguments, host: false) }; try { try { process.Start(); } catch (Win32Exception ex) when (ex.NativeErrorCode == 2 && File.Exists("/usr/bin/steam-runtime-launch-client")) { process.StartInfo = StartInfo(arguments, host: true); process.Start(); } } catch (Win32Exception ex2) { throw new InvalidOperationException($"Cannot launch the audio command '{process.StartInfo.FileName}' (error {ex2.NativeErrorCode}): {ex2.Message}. Check that PulseAudio client tools are installed and Steam's launcher service is running.", ex2); } Task<string> task = process.StandardOutput.ReadToEndAsync(); Task<string> task2 = process.StandardError.ReadToEndAsync(); if (!process.WaitForExit(3000)) { try { process.Kill(); } catch (InvalidOperationException) { } throw new TimeoutException("The audio server did not respond within 3 seconds. Check your audio service, then refresh."); } if (process.ExitCode != 0) { throw new InvalidOperationException("pactl " + string.Join(" ", arguments) + " failed: " + task2.GetAwaiter().GetResult().Trim()); } return task.GetAwaiter().GetResult(); } } internal sealed class PulseAudioBackend(int processId, Func<string[], string> command) : IAudioBackend { internal sealed class Sink(uint index, AudioDevice device) { public uint Index { get; } = index; public AudioDevice Device { get; } = device; } public AudioResult<IReadOnlyList<AudioDevice>> ListOutputs() { return AudioBoundary.Run((Func<IReadOnlyList<AudioDevice>>)(() => (from sink in ReadSinks(command(new string[3] { "--format=json", "list", "sinks" })) select sink.Device).ToArray())); } public AudioResult<string> Route(string deviceId) { return AudioBoundary.Run(delegate { string target = ((deviceId.Length == 0) ? command(new string[1] { "get-default-sink" }).Trim() : deviceId); Sink sink = ReadSinks(command(new string[3] { "--format=json", "list", "sinks" })).FirstOrDefault((Sink candidate) => candidate.Device.Id == target); if (sink == null) { return "Selected output is disconnected. Keeping your preference and waiting for it to return."; } JArray obj = JArray.Parse(command(new string[3] { "--format=json", "list", "sink-inputs" })); int num = 0; foreach (JToken item in obj) { JToken obj2 = item[(object)"properties"]; if (int.TryParse((string)((obj2 != null) ? obj2[(object)"application.process.id"] : null), out var result) && result == processId) { num++; uint num2 = RequiredIndex(item, "index"); if (RequiredIndex(item, "sink") != sink.Index) { command(new string[3] { "move-sink-input", num2.ToString(CultureInfo.InvariantCulture), sink.Device.Id }); } } } return (num != 0) ? "" : "Waiting for Valheim's audio stream. Your selection will apply when it starts."; }); } internal static IReadOnlyList<Sink> ReadSinks(string json) { List<Sink> list = new List<Sink>(); foreach (JToken item in JArray.Parse(json)) { string text = (string)item[(object)"name"]; if (string.IsNullOrWhiteSpace(text)) { throw new FormatException("The audio server returned an output without a name. Refresh to try again."); } string name = ((string)item[(object)"description"]) ?? text; list.Add(new Sink(RequiredIndex(item, "index"), new AudioDevice(text, name))); } return list; } private static uint RequiredIndex(JToken item, string name) { //IL_000e: Unknown result type (might be due to invalid IL or missing references) //IL_0014: Invalid comparison between Unknown and I4 JToken obj = item[(object)name]; if (obj == null || (int)obj.Type != 6 || !uint.TryParse(((object)item[(object)name])?.ToString(), out var result)) { throw new FormatException("The audio server returned an invalid " + name + ". Refresh to try again."); } return result; } } internal sealed class WindowsAudioBackend(int processId) : IAudioBackend { public AudioResult<IReadOnlyList<AudioDevice>> ListOutputs() { return AudioBoundary.Run(WindowsOutputs.List); } public AudioResult<string> Route(string deviceId) { return AudioBoundary.Run(delegate { using (new WindowsCom.Apartment()) { IReadOnlyList<AudioDevice> readOnlyList = WindowsOutputs.List(); if (deviceId.Length <= 0 || readOnlyList.Any((AudioDevice device) => device.Id == deviceId)) { bool flag = false; if (deviceId.Length > 0) { IReadOnlyList<string> readOnlyList2 = WindowsAudioSessions.ActiveOutputs((uint)processId, readOnlyList); if (readOnlyList2.Count == 0) { return "Waiting for Valheim's audio stream. Your selection will apply when it starts."; } flag = readOnlyList2.Any((string id) => !string.Equals(id, deviceId, StringComparison.OrdinalIgnoreCase)); } using WindowsAudioPolicy windowsAudioPolicy = new WindowsAudioPolicy(); windowsAudioPolicy.Apply((uint)processId, deviceId, flag); return flag ? "Applying saved audio output..." : ""; } return "Selected output is disconnected. Keeping your preference and waiting for it to return."; } }); } } internal sealed class WindowsAudioPolicy : IDisposable { [UnmanagedFunctionPointer(CallingConvention.StdCall)] private delegate int SetEndpoint(IntPtr self, uint pid, int flow, int role, IntPtr device); [UnmanagedFunctionPointer(CallingConvention.StdCall)] private delegate int GetEndpoint(IntPtr self, uint pid, int flow, int role, out IntPtr device); private sealed class HString : IDisposable { public IntPtr Pointer { get; } public HString(string value) { if (value.Length != 0) { WindowsCom.Check(WindowsCreateString(value, (uint)value.Length, out var handle), "Prepare Windows audio identifier"); Pointer = handle; } } public void Dispose() { if (Pointer != IntPtr.Zero) { WindowsDeleteString(Pointer); } } } private readonly WindowsCom factory; private const string RuntimeClass = "Windows.Media.Internal.AudioPolicyConfig"; private const string Prefix = "\\\\?\\SWD#MMDEVAPI#"; private const string Suffix = "#{e6327cad-dcec-4949-ae8a-991e976a79d2}"; [DllImport("combase.dll", ExactSpelling = true)] private static extern int WindowsCreateString([MarshalAs(UnmanagedType.LPWStr)] string value, uint length, out IntPtr handle); [DllImport("combase.dll", ExactSpelling = true)] private static extern int WindowsDeleteString(IntPtr handle); [DllImport("combase.dll", ExactSpelling = true)] private static extern IntPtr WindowsGetStringRawBuffer(IntPtr handle, out uint length); [DllImport("combase.dll", ExactSpelling = true)] private static extern int RoGetActivationFactory(IntPtr name, ref Guid iid, out IntPtr factory); public WindowsAudioPolicy() { using HString hString = new HString("Windows.Media.Internal.AudioPolicyConfig"); Guid iid = new Guid("AB3D4648-E242-459F-B02F-541C70306324"); IntPtr pointer; int num = RoGetActivationFactory(hString.Pointer, ref iid, out pointer); if (num < 0) { Guid iid2 = new Guid("2A59116D-6C4F-45E0-A74F-707E3FEF9258"); num = RoGetActivationFactory(hString.Pointer, ref iid2, out pointer); } WindowsCom.Check(num, "Open Windows per-app audio policy"); factory = new WindowsCom(pointer); } private string Read(uint pid, int role) { WindowsCom.Check(factory.Method<GetEndpoint>(26)(factory.Pointer, pid, 0, role, out var device), "Read Valheim output preference"); try { uint length; IntPtr ptr = WindowsGetStringRawBuffer(device, out length); return (length == 0) ? "" : (Marshal.PtrToStringUni(ptr, checked((int)length)) ?? ""); } finally { WindowsDeleteString(device); } } private int Write(uint pid, int role, string endpoint) { using HString hString = new HString(endpoint); return factory.Method<SetEndpoint>(25)(factory.Pointer, pid, 0, role, hString.Pointer); } public void Apply(uint pid, string id, bool refresh = false) { Apply((id.Length == 0) ? "" : ("\\\\?\\SWD#MMDEVAPI#" + id + "#{e6327cad-dcec-4949-ae8a-991e976a79d2}"), (int role) => Read(pid, role), (int role, string value) => Write(pid, role, value), refresh); } internal static void Apply(string endpoint, Func<int, string> read, Func<int, string, int> write, bool refresh) { string console = read(0); string multimedia = read(1); bool flag = string.Equals(console, endpoint, StringComparison.OrdinalIgnoreCase) && string.Equals(multimedia, endpoint, StringComparison.OrdinalIgnoreCase); if (!flag || refresh) { if (flag && endpoint.Length > 0) { WritePair(""); } WritePair(endpoint); } void WritePair(string value) { int num = write(0, value); if (num >= 0) { num = write(1, value); } if (num < 0) { int num2 = write(0, console); int num3 = write(1, multimedia); if (num2 < 0 || num3 < 0) { throw new COMException($"Windows could not apply or restore Valheim's output preferences (console 0x{num2:X8}, multimedia 0x{num3:X8}). Select System default or use Windows Volume mixer.", num); } WindowsCom.Check(num, "Set Valheim output (previous preferences restored)"); } } } public void Dispose() { factory.Dispose(); } } internal static class WindowsAudioSessions { [UnmanagedFunctionPointer(CallingConvention.StdCall)] private delegate int GetDevice(IntPtr self, [MarshalAs(UnmanagedType.LPWStr)] string id, out IntPtr device); [UnmanagedFunctionPointer(CallingConvention.StdCall)] private delegate int Activate(IntPtr self, ref Guid iid, uint context, IntPtr parameters, out IntPtr instance); [UnmanagedFunctionPointer(CallingConvention.StdCall)] private delegate int GetEnumerator(IntPtr self, out IntPtr sessions); [UnmanagedFunctionPointer(CallingConvention.StdCall)] private delegate int GetCount(IntPtr self, out int count); [UnmanagedFunctionPointer(CallingConvention.StdCall)] private delegate int GetSession(IntPtr self, int index, out IntPtr session); [UnmanagedFunctionPointer(CallingConvention.StdCall)] private delegate int GetProcessId(IntPtr self, out uint pid); [UnmanagedFunctionPointer(CallingConvention.StdCall)] private delegate int GetState(IntPtr self, out int state); internal static IReadOnlyList<string> ActiveOutputs(uint processId, IReadOnlyList<AudioDevice> outputs) { Guid clsid = new Guid("BCDE0395-E52F-467C-8E3D-C4579291692E"); Guid iid = new Guid("A95664D2-9614-4F35-A746-DE8DB63617E6"); WindowsCom.Check(WindowsCom.CoCreateInstance(ref clsid, IntPtr.Zero, 1u, ref iid, out var instance), "Discover Valheim audio sessions"); using WindowsCom windowsCom = new WindowsCom(instance); List<string> list = new List<string>(); foreach (AudioDevice output in outputs) { WindowsCom.Check(windowsCom.Method<GetDevice>(5)(instance, output.Id, out var device), "Open Windows audio output for session inspection"); using WindowsCom windowsCom2 = new WindowsCom(device); Guid iid2 = new Guid("77AA99A0-1BD6-484F-8BC7-2C654C9A9B6F"); WindowsCom.Check(windowsCom2.Method<Activate>(3)(device, ref iid2, 1u, IntPtr.Zero, out var instance2), "Inspect Windows audio sessions"); using WindowsCom windowsCom3 = new WindowsCom(instance2); WindowsCom.Check(windowsCom3.Method<GetEnumerator>(5)(instance2, out var sessions), "Enumerate Windows audio sessions"); using WindowsCom windowsCom4 = new WindowsCom(sessions); WindowsCom.Check(windowsCom4.Method<GetCount>(3)(sessions, out var count), "Count Windows audio sessions"); for (int i = 0; i < count; i++) { WindowsCom.Check(windowsCom4.Method<GetSession>(4)(sessions, i, out var session), "Read Windows audio session"); using (new WindowsCom(session)) { Guid iid3 = new Guid("BFB7FF88-7239-4FC9-8FA2-07C950BE9C6D"); WindowsCom.Check(Marshal.QueryInterface(session, in iid3, out var ppv), "Inspect audio session owner"); using WindowsCom windowsCom5 = new WindowsCom(ppv); uint pid; int num = windowsCom5.Method<GetProcessId>(14)(ppv, out pid); WindowsCom.Check(num, "Read audio session process ID"); if (num == 0 && pid == processId) { WindowsCom.Check(windowsCom5.Method<GetState>(3)(ppv, out var state), "Read Valheim audio session state"); if (state == 1) { list.Add(output.Id); break; } } } } } return list; } } internal sealed class WindowsCom(IntPtr pointer) : IDisposable { internal sealed class Apartment : IDisposable { private readonly bool initialized; public Apartment() { int num = RoInitialize(1u); if (num != -2147417850) { Check(num, "Initialize Windows audio COM"); } initialized = num >= 0; } public void Dispose() { if (initialized) { RoUninitialize(); } } } public IntPtr Pointer { get; private set; } = pointer; public T Method<T>(int slot) where T : Delegate { return Marshal.GetDelegateForFunctionPointer<T>(Marshal.ReadIntPtr(Marshal.ReadIntPtr(Pointer), slot * IntPtr.Size)); } public void Dispose() { if (!(Pointer == IntPtr.Zero)) { Marshal.Release(Pointer); Pointer = IntPtr.Zero; } } public static void Check(int result, string operation) { if (result < 0) { throw new COMException($"{operation} failed (0x{result:X8}). Check Windows audio settings and refresh.", result); } } [DllImport("combase.dll", ExactSpelling = true)] private static extern int RoInitialize(uint mode); [DllImport("combase.dll", ExactSpelling = true)] private static extern void RoUninitialize(); [DllImport("ole32.dll", ExactSpelling = true)] internal static extern int CoCreateInstance(ref Guid clsid, IntPtr outer, uint context, ref Guid iid, out IntPtr instance); } internal static class WindowsOutputs { [UnmanagedFunctionPointer(CallingConvention.StdCall)] private delegate int EnumEndpoints(IntPtr self, int flow, uint mask, out IntPtr collection); [UnmanagedFunctionPointer(CallingConvention.StdCall)] private delegate int GetCount(IntPtr self, out uint count); [UnmanagedFunctionPointer(CallingConvention.StdCall)] private delegate int GetItem(IntPtr self, uint index, out IntPtr device); [UnmanagedFunctionPointer(CallingConvention.StdCall)] private delegate int GetId(IntPtr self, out IntPtr id); [UnmanagedFunctionPointer(CallingConvention.StdCall)] private delegate int OpenProperties(IntPtr self, uint access, out IntPtr properties); [UnmanagedFunctionPointer(CallingConvention.StdCall)] private delegate int GetValue(IntPtr self, ref PropertyKey key, out PropVariant value); private struct PropertyKey { public Guid Format; public uint Id; } [StructLayout(LayoutKind.Explicit, Size = 24)] private struct PropVariant { [FieldOffset(0)] public ushort Type; [FieldOffset(8)] public IntPtr String; } [DllImport("ole32.dll", ExactSpelling = true)] private static extern int PropVariantClear(ref PropVariant value); public static IReadOnlyList<AudioDevice> List() { using (new WindowsCom.Apartment()) { Guid clsid = new Guid("BCDE0395-E52F-467C-8E3D-C4579291692E"); Guid iid = new Guid("A95664D2-9614-4F35-A746-DE8DB63617E6"); WindowsCom.Check(WindowsCom.CoCreateInstance(ref clsid, IntPtr.Zero, 1u, ref iid, out var instance), "Discover Windows outputs"); using WindowsCom windowsCom = new WindowsCom(instance); WindowsCom.Check(windowsCom.Method<EnumEndpoints>(3)(instance, 0, 1u, out var collection), "List Windows outputs"); using WindowsCom windowsCom2 = new WindowsCom(collection); WindowsCom.Check(windowsCom2.Method<GetCount>(3)(collection, out var count), "Count Windows outputs"); List<AudioDevice> list = new List<AudioDevice>(); for (uint num = 0u; num < count; num++) { WindowsCom.Check(windowsCom2.Method<GetItem>(4)(collection, num, out var device), "Read Windows output"); using WindowsCom windowsCom3 = new WindowsCom(device); WindowsCom.Check(windowsCom3.Method<GetId>(5)(device, out var id), "Read Windows output ID"); string text; try { text = Marshal.PtrToStringUni(id) ?? throw new InvalidOperationException("Windows returned an empty output ID."); } finally { Marshal.FreeCoTaskMem(id); } WindowsCom.Check(windowsCom3.Method<OpenProperties>(4)(device, 0u, out var properties), "Read Windows output properties"); using WindowsCom windowsCom4 = new WindowsCom(properties); PropertyKey key = new PropertyKey { Format = new Guid("A45C254E-DF1C-4EFD-8020-67D146A850E0"), Id = 14u }; WindowsCom.Check(windowsCom4.Method<GetValue>(5)(properties, ref key, out var value), "Read Windows output name"); try { string name = ((value.Type == 31) ? (Marshal.PtrToStringUni(value.String) ?? text) : text); list.Add(new AudioDevice(text, name)); } finally { PropVariantClear(ref value); } } return list; } } } }