Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
174 changes: 97 additions & 77 deletions SoundSwitch.Common/Framework/Audio/Icon/AudioDeviceIconExtractor.cs
Original file line number Diff line number Diff line change
@@ -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
{
/// <summary>
/// Extracts icons for audio devices with DataFlow-specific fallback defaults.
/// Delegates caching and GDI reference counting to <see cref="IconExtractor"/>.
/// </summary>
public class AudioDeviceIconExtractor
{
private static readonly IconHandle DefaultSpeakersHandle = IconExtractor.CreatePermanent(Resources.defaultSpeakers);
private static readonly IconHandle DefaultMicrophoneHandle = IconExtractor.CreatePermanent(Resources.defaultMicrophone);

/// <summary>
/// Extract an icon from an audio device icon path, falling back to a DataFlow-specific
/// default icon on failure.
/// </summary>
/// <param name="path">Audio device icon path (a <c>.ico</c> file or <c>dllPath,iconIndex</c>).</param>
/// <param name="dataFlow">Data flow of the device, used to select the fallback icon.</param>
/// <param name="largeIcon">When <see langword="true"/>, extract a 32×32 icon; otherwise 16×16.</param>
/// <returns>
/// An <see cref="IconHandle"/> the caller <strong>must dispose</strong> when done.
/// </returns>
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()
};
}
}

/// <summary>
/// Extract the icon out of an <see cref="MMDevice"/>.
/// </summary>
/// <param name="audioDevice"></param>
/// <param name="largeIcon"></param>
/// <returns>
/// An <see cref="IconHandle"/> the caller <strong>must dispose</strong> when done.
/// </returns>
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
{
/// <summary>
/// Extracts icons for audio devices with DataFlow-specific fallback defaults.
/// Delegates caching and GDI reference counting to <see cref="IconExtractor"/>.
/// </summary>
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));

/// <summary>
/// Creates a permanent icon handle from a bundled icon resource with a fallback option.
/// </summary>
/// <param name="bundledIconFactory">Factory function that provides the primary bundled icon.</param>
/// <param name="fallbackIconFactory">Factory function that provides a fallback system icon if the bundled icon fails to load.</param>
/// <param name="resourceName">The name of the resource being loaded, used for logging purposes.</param>
/// <returns>A permanent <see cref="IconHandle"/> that does not require disposal.</returns>
private static IconHandle CreatePermanentDefaultIcon(Func<System.Drawing.Icon> bundledIconFactory, Func<System.Drawing.Icon> 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());
}
}

/// <summary>
/// Extract an icon from an audio device icon path, falling back to a DataFlow-specific
/// default icon on failure.
/// </summary>
/// <param name="path">Audio device icon path (a <c>.ico</c> file or <c>dllPath,iconIndex</c>).</param>
/// <param name="dataFlow">Data flow of the device, used to select the fallback icon.</param>
/// <param name="largeIcon">When <see langword="true"/>, extract a 32×32 icon; otherwise 16×16.</param>
/// <returns>
/// An <see cref="IconHandle"/> the caller <strong>must dispose</strong> when done.
/// </returns>
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()
};
}
}

/// <summary>
/// Extract the icon out of an <see cref="MMDevice"/>.
/// </summary>
/// <param name="audioDevice"></param>
/// <param name="largeIcon"></param>
/// <returns>
/// An <see cref="IconHandle"/> the caller <strong>must dispose</strong> when done.
/// </returns>
public static IconHandle ExtractIconFromAudioDevice(MMDevice audioDevice, bool largeIcon)
{
return ExtractIconFromPath(audioDevice.IconPath, audioDevice.DataFlow, largeIcon);
}
}
}
27 changes: 27 additions & 0 deletions SoundSwitch.Tests/AudioDeviceIconExtractorTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
using FluentAssertions;

using NAudio.CoreAudioApi;

using NUnit.Framework;

using SoundSwitch.Common.Framework.Audio.Icon;

namespace SoundSwitch.Tests;

/// <summary>
/// Tests for the <see cref="AudioDeviceIconExtractor"/> class, verifying icon extraction
/// behavior for audio devices including fallback handling for invalid icon paths.
/// </summary>
[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();
}
}
Loading