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.
This commit is contained in:
@@ -0,0 +1,96 @@
|
||||
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);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user