From aef8895dbe0ea13d2d424b537fc012769f554eff Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 23 Jun 2026 11:57:08 +0000 Subject: [PATCH 1/3] Initial plan From 12b35d9b5d5534d3261c90d307e88980310b5336 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 23 Jun 2026 12:03:19 +0000 Subject: [PATCH 2/3] fix(common): avoid icon extractor type-init crash on invalid fallback resources Co-authored-by: Belphemur <197810+Belphemur@users.noreply.github.com> --- .../Audio/Icon/AudioDeviceIconExtractor.cs | 17 ++++++++++++-- .../AudioDeviceIconExtractorTests.cs | 23 +++++++++++++++++++ 2 files changed, 38 insertions(+), 2 deletions(-) create mode 100644 SoundSwitch.Tests/AudioDeviceIconExtractorTests.cs diff --git a/SoundSwitch.Common/Framework/Audio/Icon/AudioDeviceIconExtractor.cs b/SoundSwitch.Common/Framework/Audio/Icon/AudioDeviceIconExtractor.cs index d24adaa70c..e41e3c90f7 100644 --- a/SoundSwitch.Common/Framework/Audio/Icon/AudioDeviceIconExtractor.cs +++ b/SoundSwitch.Common/Framework/Audio/Icon/AudioDeviceIconExtractor.cs @@ -30,8 +30,21 @@ namespace SoundSwitch.Common.Framework.Audio.Icon /// public class AudioDeviceIconExtractor { - private static readonly IconHandle DefaultSpeakersHandle = IconExtractor.CreatePermanent(Resources.defaultSpeakers); - private static readonly IconHandle DefaultMicrophoneHandle = IconExtractor.CreatePermanent(Resources.defaultMicrophone); + private static readonly IconHandle DefaultSpeakersHandle = CreatePermanentDefaultIcon(() => Resources.defaultSpeakers, () => System.Drawing.SystemIcons.Application, nameof(Resources.defaultSpeakers)); + private static readonly IconHandle DefaultMicrophoneHandle = CreatePermanentDefaultIcon(() => Resources.defaultMicrophone, () => System.Drawing.SystemIcons.Information, nameof(Resources.defaultMicrophone)); + + private static IconHandle CreatePermanentDefaultIcon(Func bundledIconFactory, Func fallbackIconFactory, string resourceName) + { + try + { + return IconExtractor.CreatePermanent(bundledIconFactory()); + } + catch (Exception e) + { + Log.Warning(e, "Can't load bundled fallback icon {resourceName}, using system icon fallback", resourceName); + return IconExtractor.CreatePermanent((System.Drawing.Icon)fallbackIconFactory().Clone()); + } + } /// /// Extract an icon from an audio device icon path, falling back to a DataFlow-specific diff --git a/SoundSwitch.Tests/AudioDeviceIconExtractorTests.cs b/SoundSwitch.Tests/AudioDeviceIconExtractorTests.cs new file mode 100644 index 0000000000..7c027878a8 --- /dev/null +++ b/SoundSwitch.Tests/AudioDeviceIconExtractorTests.cs @@ -0,0 +1,23 @@ +using FluentAssertions; + +using NAudio.CoreAudioApi; + +using NUnit.Framework; + +using SoundSwitch.Common.Framework.Audio.Icon; + +namespace SoundSwitch.Tests; + +[TestFixture] +public class AudioDeviceIconExtractorTests +{ + [TestCase(DataFlow.Render)] + [TestCase(DataFlow.Capture)] + public void ExtractIconFromPath_WhenPathIsInvalid_ReturnsFallbackIcon(DataFlow dataFlow) + { + using var iconHandle = AudioDeviceIconExtractor.ExtractIconFromPath("invalid-icon-path", dataFlow, false); + + iconHandle.Should().NotBeNull(); + iconHandle.Icon.Should().NotBeNull(); + } +} From c4c0b2902837d27329a6442254dcb4c3e12ac869 Mon Sep 17 00:00:00 2001 From: "coderabbitai[bot]" <136622811+coderabbitai[bot]@users.noreply.github.com> Date: Tue, 23 Jun 2026 12:35:38 +0000 Subject: [PATCH 3/3] fix: apply CodeRabbit auto-fixes Fixed 2 file(s) based on 2 unresolved review comments. Co-authored-by: CodeRabbit --- .../Audio/Icon/AudioDeviceIconExtractor.cs | 187 +++++++++--------- .../AudioDeviceIconExtractorTests.cs | 50 ++--- 2 files changed, 124 insertions(+), 113 deletions(-) diff --git a/SoundSwitch.Common/Framework/Audio/Icon/AudioDeviceIconExtractor.cs b/SoundSwitch.Common/Framework/Audio/Icon/AudioDeviceIconExtractor.cs index e41e3c90f7..13c4481fdc 100644 --- a/SoundSwitch.Common/Framework/Audio/Icon/AudioDeviceIconExtractor.cs +++ b/SoundSwitch.Common/Framework/Audio/Icon/AudioDeviceIconExtractor.cs @@ -1,90 +1,97 @@ -/******************************************************************** - * Copyright (C) 2015-2017 Antoine Aflalo - * - * 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 2 - * 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. - ********************************************************************/ - -#nullable enable -using System; - -using NAudio.CoreAudioApi; - -using Serilog; - -using SoundSwitch.Common.Framework.Icon; -using SoundSwitch.Common.Properties; - -namespace SoundSwitch.Common.Framework.Audio.Icon -{ - /// - /// Extracts icons for audio devices with DataFlow-specific fallback defaults. - /// Delegates caching and GDI reference counting to . - /// - public class AudioDeviceIconExtractor - { - private static readonly IconHandle DefaultSpeakersHandle = CreatePermanentDefaultIcon(() => Resources.defaultSpeakers, () => System.Drawing.SystemIcons.Application, nameof(Resources.defaultSpeakers)); - private static readonly IconHandle DefaultMicrophoneHandle = CreatePermanentDefaultIcon(() => Resources.defaultMicrophone, () => System.Drawing.SystemIcons.Information, nameof(Resources.defaultMicrophone)); - - private static IconHandle CreatePermanentDefaultIcon(Func bundledIconFactory, Func fallbackIconFactory, string resourceName) - { - try - { - return IconExtractor.CreatePermanent(bundledIconFactory()); - } - catch (Exception e) - { - Log.Warning(e, "Can't load bundled fallback icon {resourceName}, using system icon fallback", resourceName); - return IconExtractor.CreatePermanent((System.Drawing.Icon)fallbackIconFactory().Clone()); - } - } - - /// - /// Extract an icon from an audio device icon path, falling back to a DataFlow-specific - /// default icon on failure. - /// - /// Audio device icon path (a .ico file or dllPath,iconIndex). - /// Data flow of the device, used to select the fallback icon. - /// When , extract a 32×32 icon; otherwise 16×16. - /// - /// An the caller must dispose when done. - /// - public static IconHandle ExtractIconFromPath(string path, DataFlow dataFlow, bool largeIcon) - { - try - { - return IconExtractor.ExtractFromPath(path, largeIcon); - } - catch (Exception e) - { - Log.Warning(e, "Can't extract icon from {path}", path); - return dataFlow switch - { - DataFlow.Capture => DefaultMicrophoneHandle.Acquire(), - DataFlow.Render => DefaultSpeakersHandle.Acquire(), - _ => throw new ArgumentOutOfRangeException() - }; - } - } - - /// - /// Extract the icon out of an . - /// - /// - /// - /// - /// An the caller must dispose when done. - /// - public static IconHandle ExtractIconFromAudioDevice(MMDevice audioDevice, bool largeIcon) - { - return ExtractIconFromPath(audioDevice.IconPath, audioDevice.DataFlow, largeIcon); - } - } -} +/******************************************************************** + * Copyright (C) 2015-2017 Antoine Aflalo + * + * 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 2 + * 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. + ********************************************************************/ + +#nullable enable +using System; + +using NAudio.CoreAudioApi; + +using Serilog; + +using SoundSwitch.Common.Framework.Icon; +using SoundSwitch.Common.Properties; + +namespace SoundSwitch.Common.Framework.Audio.Icon +{ + /// + /// Extracts icons for audio devices with DataFlow-specific fallback defaults. + /// Delegates caching and GDI reference counting to . + /// + public class AudioDeviceIconExtractor + { + private static readonly IconHandle DefaultSpeakersHandle = CreatePermanentDefaultIcon(() => Resources.defaultSpeakers, () => System.Drawing.SystemIcons.Application, nameof(Resources.defaultSpeakers)); + private static readonly IconHandle DefaultMicrophoneHandle = CreatePermanentDefaultIcon(() => Resources.defaultMicrophone, () => System.Drawing.SystemIcons.Information, nameof(Resources.defaultMicrophone)); + + /// + /// Creates a permanent icon handle from a bundled icon resource with a fallback option. + /// + /// Factory function that provides the primary bundled icon. + /// Factory function that provides a fallback system icon if the bundled icon fails to load. + /// The name of the resource being loaded, used for logging purposes. + /// A permanent that does not require disposal. + private static IconHandle CreatePermanentDefaultIcon(Func bundledIconFactory, Func fallbackIconFactory, string resourceName) + { + try + { + return IconExtractor.CreatePermanent(bundledIconFactory()); + } + catch (Exception e) + { + Log.Warning(e, "Can't load bundled fallback icon {resourceName}, using system icon fallback", resourceName); + return IconExtractor.CreatePermanent((System.Drawing.Icon)fallbackIconFactory().Clone()); + } + } + + /// + /// Extract an icon from an audio device icon path, falling back to a DataFlow-specific + /// default icon on failure. + /// + /// Audio device icon path (a .ico file or dllPath,iconIndex). + /// Data flow of the device, used to select the fallback icon. + /// When , extract a 32×32 icon; otherwise 16×16. + /// + /// An the caller must dispose when done. + /// + public static IconHandle ExtractIconFromPath(string path, DataFlow dataFlow, bool largeIcon) + { + try + { + return IconExtractor.ExtractFromPath(path, largeIcon); + } + catch (Exception e) + { + Log.Warning(e, "Can't extract icon from {path}", path); + return dataFlow switch + { + DataFlow.Capture => DefaultMicrophoneHandle.Acquire(), + DataFlow.Render => DefaultSpeakersHandle.Acquire(), + _ => throw new ArgumentOutOfRangeException() + }; + } + } + + /// + /// Extract the icon out of an . + /// + /// + /// + /// + /// An the caller must dispose when done. + /// + public static IconHandle ExtractIconFromAudioDevice(MMDevice audioDevice, bool largeIcon) + { + return ExtractIconFromPath(audioDevice.IconPath, audioDevice.DataFlow, largeIcon); + } + } +} diff --git a/SoundSwitch.Tests/AudioDeviceIconExtractorTests.cs b/SoundSwitch.Tests/AudioDeviceIconExtractorTests.cs index 7c027878a8..b74ac978ad 100644 --- a/SoundSwitch.Tests/AudioDeviceIconExtractorTests.cs +++ b/SoundSwitch.Tests/AudioDeviceIconExtractorTests.cs @@ -1,23 +1,27 @@ -using FluentAssertions; - -using NAudio.CoreAudioApi; - -using NUnit.Framework; - -using SoundSwitch.Common.Framework.Audio.Icon; - -namespace SoundSwitch.Tests; - -[TestFixture] -public class AudioDeviceIconExtractorTests -{ - [TestCase(DataFlow.Render)] - [TestCase(DataFlow.Capture)] - public void ExtractIconFromPath_WhenPathIsInvalid_ReturnsFallbackIcon(DataFlow dataFlow) - { - using var iconHandle = AudioDeviceIconExtractor.ExtractIconFromPath("invalid-icon-path", dataFlow, false); - - iconHandle.Should().NotBeNull(); - iconHandle.Icon.Should().NotBeNull(); - } -} +using FluentAssertions; + +using NAudio.CoreAudioApi; + +using NUnit.Framework; + +using SoundSwitch.Common.Framework.Audio.Icon; + +namespace SoundSwitch.Tests; + +/// +/// Tests for the class, verifying icon extraction +/// behavior for audio devices including fallback handling for invalid icon paths. +/// +[TestFixture] +public class AudioDeviceIconExtractorTests +{ + [TestCase(DataFlow.Render)] + [TestCase(DataFlow.Capture)] + public void ExtractIconFromPath_WhenPathIsInvalid_ReturnsFallbackIcon(DataFlow dataFlow) + { + using var iconHandle = AudioDeviceIconExtractor.ExtractIconFromPath("invalid-icon-path", dataFlow, false); + + iconHandle.Should().NotBeNull(); + iconHandle.Icon.Should().NotBeNull(); + } +}