Files
windows/SystemVolume.cs
T
ros d5e827112a Add MusicBridge protocol documentation and implement system volume control
- Created PROTOCOL.md to outline the MusicBridge protocol v1, detailing setup, TLS, pairing, state management, and command structure.
- Implemented SystemVolume class for managing system audio levels, including methods for setting, reading, changing, and muting volume.
- Added unit tests for the MusicBridge agent, covering pairing, identity storage, command validation, and media state management.
- Included tests for real-time media reading from Windows and ensured proper handling of artwork caching and retrieval.
2026-09-09 01:05:52 +03:00

97 lines
3.9 KiB
C#

using System.Runtime.InteropServices;
namespace MusicBridge.Agent;
// Obtain the current default output for each operation, including after device changes.
internal static class SystemVolume
{
public static void Set(float level) => WithEndpoint(endpoint =>
{
if (!float.IsFinite(level) || level < 0 || level > 1)
throw new ArgumentOutOfRangeException(nameof(level));
var context = Guid.Empty;
endpoint.SetMasterVolumeLevelScalar(level, ref context);
return true;
});
public static (float Level, bool Muted) Read() => WithEndpoint(endpoint =>
{
endpoint.GetMasterVolumeLevelScalar(out var level);
endpoint.GetMute(out var muted);
return (level, muted);
});
public static void Change(float delta) => WithEndpoint(endpoint =>
{
endpoint.GetMasterVolumeLevelScalar(out var level);
var context = Guid.Empty;
endpoint.SetMasterVolumeLevelScalar(Math.Clamp(level + delta, 0f, 1f), ref context);
return true;
});
public static void ToggleMute() => WithEndpoint(endpoint =>
{
endpoint.GetMute(out var muted);
var context = Guid.Empty;
endpoint.SetMute(!muted, ref context);
return true;
});
private static T WithEndpoint<T>(Func<IAudioEndpointVolume, T> action)
{
var enumerator = (IMMDeviceEnumerator)new MMDeviceEnumerator();
IMMDevice? device = null;
object? volume = null;
try
{
enumerator.GetDefaultAudioEndpoint(0, 1, out device); // Render, Multimedia
var iid = typeof(IAudioEndpointVolume).GUID;
device.Activate(ref iid, 23, IntPtr.Zero, out volume);
return action((IAudioEndpointVolume)volume);
}
finally
{
if (volume is not null) Marshal.ReleaseComObject(volume);
if (device is not null) Marshal.ReleaseComObject(device);
Marshal.ReleaseComObject(enumerator);
}
}
[ComImport, Guid("BCDE0395-E52F-467C-8E3D-C4579291692E")]
private class MMDeviceEnumerator { }
// COM declarations must preserve the native vtable order, including unused methods.
// HRESULT failures are translated to exceptions by the COM interop runtime.
[ComImport, Guid("A95664D2-9614-4F35-A746-DE8DB63617E6"), InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
private interface IMMDeviceEnumerator
{
void EnumAudioEndpoints(int flow, uint mask, out IntPtr devices);
void GetDefaultAudioEndpoint(int flow, int role, out IMMDevice device);
}
[ComImport, Guid("D666063F-1587-4E43-81F1-B948E807363F"), InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
private interface IMMDevice
{
void Activate(ref Guid iid, uint context, IntPtr activation,
[MarshalAs(UnmanagedType.IUnknown)] out object instance);
}
[ComImport, Guid("5CDF2C82-841E-4546-9722-0CF74078229A"), InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
private interface IAudioEndpointVolume
{
void RegisterControlChangeNotify(IntPtr callback);
void UnregisterControlChangeNotify(IntPtr callback);
void GetChannelCount(out uint count);
void SetMasterVolumeLevel(float level, ref Guid context);
void SetMasterVolumeLevelScalar(float level, ref Guid context);
void GetMasterVolumeLevel(out float level);
void GetMasterVolumeLevelScalar(out float level);
void SetChannelVolumeLevel(uint channel, float level, ref Guid context);
void SetChannelVolumeLevelScalar(uint channel, float level, ref Guid context);
void GetChannelVolumeLevel(uint channel, out float level);
void GetChannelVolumeLevelScalar(uint channel, out float level);
void SetMute([MarshalAs(UnmanagedType.Bool)] bool muted, ref Guid context);
void GetMute([MarshalAs(UnmanagedType.Bool)] out bool muted);
}
}