Skip to content

Commit 1500c38

Browse files
authored
Merge branch 'dev' into dependabot/nuget/Microsoft.Extensions.Caching.Memory-9.0.3
2 parents 8591cfa + b464e2d commit 1500c38

File tree

11 files changed

+349
-23
lines changed

11 files changed

+349
-23
lines changed

.github/copilot-instructions.md

+97
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
# SoundSwitch GitHub Copilot Instructions
2+
3+
## Project Architecture
4+
5+
SoundSwitch is a Windows application for switching audio playback and recording devices using hotkeys. It's built with .NET on the Windows platform.
6+
7+
### Core Components
8+
9+
- **SoundSwitch (Main Project)**: The core application handling UI, notifications, and user interaction
10+
- **SoundSwitch.Audio.Manager**: Manages audio device switching using NAudio and Windows APIs
11+
- **SoundSwitch.Common**: Shared utilities and framework components
12+
- **SoundSwitch.CLI**: Command-line interface for controlling SoundSwitch
13+
- **SoundSwitch.IPC**: Inter-process communication for integration with other applications
14+
- **SoundSwitch.UI.Menu**: UI menu components for the system tray and application
15+
- **SoundSwitch.Bluetooth**: Bluetooth device management
16+
- **SoundSwitch.UI.UserControls**: Reusable UI controls
17+
18+
### Architectural Patterns
19+
20+
- **Model-View-Controller**: The application follows MVC pattern with separation of concerns
21+
- **Event-driven architecture**: Heavy use of event handling for device changes and hotkey detection
22+
- **Dependency Injection**: Used to manage component dependencies
23+
- **Job Scheduling**: Used for background tasks and recurring operations
24+
25+
## Development Guidelines
26+
27+
### Coding Standards
28+
29+
1. Classes should use proper XML documentation
30+
2. Follow C# naming conventions (PascalCase for public members, camelCase for private)
31+
3. Extract interfaces for testable components
32+
4. Include copyright notices at the top of source files
33+
34+
### Error Handling
35+
36+
- Use structured exception handling with appropriate logging
37+
- Prefer using the `Result<T>` pattern from RailSharp for indicating success/failure
38+
- Log errors with Serilog
39+
40+
### Localization
41+
42+
- All user-facing strings should be localized
43+
- Use resource files (.resx) for localization
44+
- Support for right-to-left languages
45+
46+
### Audio Device Management
47+
48+
- Use NAudio for audio device enumeration and control
49+
- Handle device addition/removal events
50+
- Support both playback and recording devices
51+
52+
### Settings Management
53+
54+
- Use AppConfigs.Configuration for persistent settings
55+
- Settings should be automatically saved when changed
56+
57+
### Hotkey Registration
58+
59+
- Use the WindowsAPIAdapter for hotkey registration
60+
- Handle conflicts with other applications
61+
- Support customizable hotkeys
62+
63+
## Project Structure
64+
65+
- Use separate assembly projects for distinct functionality
66+
- Tests should be in corresponding test projects
67+
- UI components should be separated from business logic
68+
69+
## Important Interfaces
70+
71+
- `IAudioDeviceLister`: For enumerating audio devices
72+
- `IAppModel`: Main model interface for application state
73+
- `INotificationManager`: Handles user notifications
74+
- `IProfileManager`: Manages device switching profiles
75+
76+
## Testing
77+
78+
- Use unit tests for core functionality
79+
- Mock external dependencies
80+
- Test all device operations
81+
82+
## Deployment
83+
84+
- Support multiple release channels (Stable, Beta, Nightly)
85+
- Use GitHub Actions for CI/CD
86+
- Sign executables with certificate
87+
88+
## When Contributing
89+
90+
1. Target the latest .NET version supported by the project
91+
2. Test changes thoroughly across different Windows versions
92+
3. Use GitHub flow for contributions (feature branches and PRs)
93+
4. Update documentation and CHANGELOG.md with significant changes
94+
95+
## DeviceFullInfo
96+
97+
Never use the Name property. Always use the NameClean property instead. Ensure that any handling of device names is done using NameClean to avoid potential issues with formatting or invalid characters.

Directory.Packages.props

+1-1
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@
3535
<PackageVersion Include="Spectre.Console.Cli" Version="0.49.1" />
3636
<PackageVersion Include="System.CommandLine" Version="2.0.0-beta4.22272.1" />
3737
<PackageVersion Include="System.Diagnostics.TraceSource" Version="4.3.0" />
38-
<PackageVersion Include="System.Drawing.Common" Version="9.0.2" />
38+
<PackageVersion Include="System.Drawing.Common" Version="9.0.3" />
3939
<PackageVersion Include="System.Net.Http" Version="4.3.4" />
4040
<PackageVersion Include="System.Reactive.Linq" Version="6.0.1" />
4141
<PackageVersion Include="System.Resources.Extensions" Version="9.0.2" />

SoundSwitch.Audio.Manager/AudioSwitcher.cs

+1-2
Original file line numberDiff line numberDiff line change
@@ -333,8 +333,7 @@ public IEnumerable<DeviceFullInfo> GetAudioEndpoints(EDataFlow flow, EDeviceStat
333333
return null;
334334
}
335335
})
336-
.Where(device => device != null)
337-
.Where(device => !string.IsNullOrEmpty(device?.Name))
336+
.Where(device => !string.IsNullOrEmpty(device?.NameClean))
338337
.Cast<DeviceFullInfo>().ToArray();
339338
});
340339

SoundSwitch.Common/Framework/Audio/Device/DeviceFullInfo.cs

+36
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
#nullable enable
22
using System;
3+
using System.Threading.Tasks;
34
using NAudio.CoreAudioApi;
45
using Newtonsoft.Json;
56
using SoundSwitch.Common.Framework.Audio.Icon;
@@ -23,6 +24,14 @@ public class DeviceFullInfo : DeviceInfo, IDisposable
2324
[JsonIgnore]
2425
public int Volume { get; private set; } = 0;
2526

27+
[JsonIgnore]
28+
public bool IsMuted { get; private set; }
29+
30+
/// <summary>
31+
/// Event raised when the device's volume or mute state changes
32+
/// </summary>
33+
public event EventHandler<VolumeChangedEventArgs>? MuteVolumeChanged;
34+
2635
[JsonConstructor]
2736
public DeviceFullInfo(string name, string id, DataFlow type, string iconPath, DeviceState state, bool isUsb) : base(name, id, type, isUsb, DateTime.UtcNow)
2837
{
@@ -44,10 +53,12 @@ public DeviceFullInfo(MMDevice device) : base(device)
4453
if (deviceAudioEndpointVolume == null)
4554
{
4655
Volume = 0;
56+
IsMuted = false;
4757
return;
4858
}
4959

5060
Volume = (int)Math.Round(deviceAudioEndpointVolume.MasterVolumeLevelScalar * 100);
61+
IsMuted = deviceAudioEndpointVolume.Mute;
5162
deviceAudioEndpointVolume.OnVolumeNotification += DeviceAudioEndpointVolumeOnOnVolumeNotification;
5263
}
5364
}
@@ -59,7 +70,23 @@ public DeviceFullInfo(MMDevice device) : base(device)
5970

6071
private void DeviceAudioEndpointVolumeOnOnVolumeNotification(AudioVolumeNotificationData data)
6172
{
73+
// Store previous values before updating
74+
var previousVolume = Volume;
75+
var wasMuted = IsMuted;
76+
77+
// Update current values
6278
Volume = (int)Math.Round(data.MasterVolume * 100F);
79+
IsMuted = data.Muted;
80+
81+
// Only raise event if there's an actual change
82+
if (previousVolume != Volume || wasMuted != IsMuted)
83+
{
84+
Task.Run(() =>
85+
{
86+
// Trigger the event with our custom event args that includes previous values
87+
MuteVolumeChanged?.Invoke(this, new VolumeChangedEventArgs(Volume, previousVolume, IsMuted, wasMuted));
88+
});
89+
}
6390
}
6491

6592
public void Dispose()
@@ -87,6 +114,15 @@ protected virtual void Dispose(bool disposing)
87114
_device.AudioEndpointVolume.OnVolumeNotification -= DeviceAudioEndpointVolumeOnOnVolumeNotification;
88115
}
89116

117+
// Clean up event subscribers to prevent memory leaks
118+
if (MuteVolumeChanged != null)
119+
{
120+
foreach (var subscriber in MuteVolumeChanged.GetInvocationList())
121+
{
122+
MuteVolumeChanged -= (EventHandler<VolumeChangedEventArgs>)subscriber;
123+
}
124+
}
125+
90126
_device?.Dispose();
91127
}
92128
catch (Exception)

SoundSwitch.Common/Framework/Audio/Device/DeviceInfo.cs

+1
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
using System.Text.RegularExpressions;
33
using NAudio.CoreAudioApi;
44
using Newtonsoft.Json;
5+
#pragma warning disable CS0618 // Type or member is obsolete
56

67
namespace SoundSwitch.Common.Framework.Audio.Device
78
{
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
/********************************************************************
2+
* Copyright (C) 2015-2017 Antoine Aflalo
3+
*
4+
* This program is free software; you can redistribute it and/or
5+
* modify it under the terms of the GNU General Public License
6+
* as published by the Free Software Foundation; either version 2
7+
* of the License, or (at your option) any later version.
8+
*
9+
* This program is distributed in the hope that it will be useful,
10+
* but WITHOUT ANY WARRANTY; without even the implied warranty of
11+
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12+
* GNU General Public License for more details.
13+
********************************************************************/
14+
15+
using System;
16+
17+
namespace SoundSwitch.Common.Framework.Audio.Device
18+
{
19+
/// <summary>
20+
/// Payload for device volume change events
21+
/// </summary>
22+
public class DeviceVolumeChangedPayload
23+
{
24+
/// <summary>
25+
/// The device that had its volume/mute state changed
26+
/// </summary>
27+
public DeviceFullInfo Device { get; }
28+
29+
/// <summary>
30+
/// The current volume level (0-100)
31+
/// </summary>
32+
public int Volume { get; }
33+
34+
/// <summary>
35+
/// The previous volume level (0-100)
36+
/// </summary>
37+
public int PreviousVolume { get; }
38+
39+
/// <summary>
40+
/// Whether the device is currently muted
41+
/// </summary>
42+
public bool IsMuted { get; }
43+
44+
/// <summary>
45+
/// Whether the device was previously muted
46+
/// </summary>
47+
public bool WasMuted { get; }
48+
49+
/// <summary>
50+
/// Whether the volume has changed
51+
/// </summary>
52+
public bool VolumeChanged => Volume != PreviousVolume;
53+
54+
/// <summary>
55+
/// Whether the mute state has changed
56+
/// </summary>
57+
public bool MuteChanged => IsMuted != WasMuted;
58+
59+
/// <summary>
60+
/// When the change occurred
61+
/// </summary>
62+
public DateTimeOffset Timestamp { get; }
63+
64+
/// <summary>
65+
/// Creates a new DeviceVolumeChangedPayload
66+
/// </summary>
67+
/// <param name="device">The device that changed</param>
68+
/// <param name="args">The volume change event arguments</param>
69+
public DeviceVolumeChangedPayload(DeviceFullInfo device, VolumeChangedEventArgs args)
70+
{
71+
Device = device;
72+
Volume = args.Volume;
73+
PreviousVolume = args.PreviousVolume;
74+
IsMuted = args.IsMuted;
75+
WasMuted = args.WasMuted;
76+
Timestamp = DateTimeOffset.Now;
77+
}
78+
}
79+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
#nullable enable
2+
using System;
3+
4+
namespace SoundSwitch.Common.Framework.Audio.Device
5+
{
6+
/// <summary>
7+
/// Event arguments for device volume and mute state changes
8+
/// </summary>
9+
public class VolumeChangedEventArgs : EventArgs
10+
{
11+
/// <summary>
12+
/// The current volume level (0-100)
13+
/// </summary>
14+
public int Volume { get; }
15+
16+
/// <summary>
17+
/// The previous volume level (0-100)
18+
/// </summary>
19+
public int PreviousVolume { get; }
20+
21+
/// <summary>
22+
/// Whether the device is muted
23+
/// </summary>
24+
public bool IsMuted { get; }
25+
26+
/// <summary>
27+
/// Whether the device was previously muted
28+
/// </summary>
29+
public bool WasMuted { get; }
30+
31+
/// <summary>
32+
/// Whether the volume has changed
33+
/// </summary>
34+
public bool VolumeChanged => Volume != PreviousVolume;
35+
36+
/// <summary>
37+
/// Whether the mute state has changed
38+
/// </summary>
39+
public bool MuteChanged => IsMuted != WasMuted;
40+
41+
/// <summary>
42+
/// Initializes a new instance of the VolumeChangedEventArgs class
43+
/// </summary>
44+
/// <param name="volume">Current volume level (0-100)</param>
45+
/// <param name="previousVolume">Previous volume level (0-100)</param>
46+
/// <param name="isMuted">Whether the device is muted</param>
47+
/// <param name="wasMuted">Whether the device was previously muted</param>
48+
public VolumeChangedEventArgs(int volume, int previousVolume, bool isMuted, bool wasMuted)
49+
{
50+
Volume = volume;
51+
PreviousVolume = previousVolume;
52+
IsMuted = isMuted;
53+
WasMuted = wasMuted;
54+
}
55+
}
56+
}

0 commit comments

Comments
 (0)