diff --git a/SoundSwitch.Common/Framework/Audio/Icon/AudioDeviceIconExtractor.cs b/SoundSwitch.Common/Framework/Audio/Icon/AudioDeviceIconExtractor.cs
index d24adaa70c..13c4481fdc 100644
--- a/SoundSwitch.Common/Framework/Audio/Icon/AudioDeviceIconExtractor.cs
+++ b/SoundSwitch.Common/Framework/Audio/Icon/AudioDeviceIconExtractor.cs
@@ -1,77 +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 = IconExtractor.CreatePermanent(Resources.defaultSpeakers);
- private static readonly IconHandle DefaultMicrophoneHandle = IconExtractor.CreatePermanent(Resources.defaultMicrophone);
-
- ///
- /// 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
new file mode 100644
index 0000000000..b74ac978ad
--- /dev/null
+++ b/SoundSwitch.Tests/AudioDeviceIconExtractorTests.cs
@@ -0,0 +1,27 @@
+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();
+ }
+}