Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add speaker volume and speaker mute support #453

Open
wants to merge 4 commits into
base: master
Choose a base branch
from
Open
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
2 changes: 2 additions & 0 deletions Source/Monitorian.Core/AppControllerCore.cs
Original file line number Diff line number Diff line change
Expand Up @@ -358,6 +358,8 @@ protected virtual async Task ScanAsync(TimeSpan interval)
{
x.IsTarget = true;
}
x.UpdateSpeakerVolume();
x.UpdateIsSpeakerMute();
return x.IsControllable;
});
}
Expand Down
34 changes: 34 additions & 0 deletions Source/Monitorian.Core/Common/RelayCommand.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
using System;
using System.Windows.Input;

namespace Monitorian.Core.Common
{
public class RelayCommand : ICommand
{
private readonly Action _execute;
private readonly Func<bool> _canExecute;

public event EventHandler CanExecuteChanged;

public RelayCommand(Action execute)
{
_execute = execute;
}

public bool CanExecute(object parameter)
{
return _canExecute?.Invoke() != false;
}

public RelayCommand(Action execute, Func<bool> canExecute)
{
_execute = execute;
_canExecute = canExecute;
}

public void Execute(object parameter)
{
_execute();
}
}
}
86 changes: 86 additions & 0 deletions Source/Monitorian.Core/Models/Monitor/DdcMonitorItem.cs
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,8 @@ internal class DdcMonitorItem : MonitorItem
public override bool IsContrastSupported => _capability.IsContrastSupported;
public override bool IsPrecleared => _capability.IsPrecleared;
public override bool IsTemperatureSupported => _capability.IsTemperatureSupported;
public override bool IsSpeakerVolumeSupported => _capability.IsSpeakerVolumeSupported;
public override bool IsSpeakerMuteSupported => _capability.IsSpeakerMuteSupported;

public DdcMonitorItem(
string deviceInstanceId,
Expand Down Expand Up @@ -137,6 +139,90 @@ static byte GetNext(IReadOnlyList<byte> source, byte current)
}
}


private uint _minimumVolume = 0; // Raw minimum volume (may not always 0)
private uint _maximumVolume = 100; // Raw maximum volume (may not always 100)

public override AccessResult UpdateSpeakerVolume()
{
var (result, minimum, current, maximum) = MonitorConfiguration.GetSpeakerVolume(_handle);
if ((result.Status == AccessStatus.Succeeded) && (minimum < maximum) && (minimum <= current) && (current <= maximum))
{
this.SpeakerVolume = (int)Math.Round((double)(current - minimum) / (maximum - minimum) * 100D, MidpointRounding.AwayFromZero);
this._minimumVolume = minimum;
this._maximumVolume = maximum;
}
else
{
this.SpeakerVolume = -1; // Default
}
return result;
}

public override AccessResult SetSpeakerVolume(int volume)
{
if (volume is < 0 or > 100)
throw new ArgumentOutOfRangeException(nameof(volume), volume, "The volume must be from 0 to 100.");

var buffer = (uint)Math.Round(volume / 100D * (_maximumVolume - _minimumVolume) + _minimumVolume, MidpointRounding.AwayFromZero);

var result = MonitorConfiguration.SetSpeakerVolume(_handle, buffer);

if (result.IsSuccess)
{
this.SpeakerVolume = volume;
}

if (volume > 0 && IsSpeakerMute)
{
result = MonitorConfiguration.ToggleSpeakerMute(_handle, false);
if (result.IsSuccess)
{
this.IsSpeakerMute = false;
}
}

if(volume == 0 && !IsSpeakerMute)
{
result = MonitorConfiguration.ToggleSpeakerMute(_handle, true);
if (result.IsSuccess)
{
this.IsSpeakerMute = true;
}
}

return result;
}

public override AccessResult UpdateIsSpeakerMute()
{
var (result, isMute) = MonitorConfiguration.IsSpeakerMute(_handle);
if (result.Status == AccessStatus.Succeeded)
{
this.IsSpeakerMute = isMute;
}
else
{
this.IsSpeakerMute = false; // Default
}
return result;
}

public override AccessResult ToggleSpeakerMute()
{
if (!this.IsSpeakerMuteSupported)
throw new InvalidOperationException("Toggle speaker mute state is not allowed, since Audio Mute is not supported by this monitor");

var newState = !this.IsSpeakerMute;
var result = MonitorConfiguration.ToggleSpeakerMute(_handle, newState);
if (result.Status == AccessStatus.Succeeded)
{
this.IsSpeakerMute = newState;
}
return result;
}


#region IDisposable

private bool _isDisposed = false;
Expand Down
12 changes: 12 additions & 0 deletions Source/Monitorian.Core/Models/Monitor/IMonitor.cs
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@ public interface IMonitor : IDisposable
bool IsBrightnessSupported { get; }
bool IsContrastSupported { get; }
bool IsTemperatureSupported { get; }
bool IsSpeakerVolumeSupported { get; }
bool IsSpeakerMuteSupported { get; }

int Brightness { get; }
int BrightnessSystemAdjusted { get; }
Expand All @@ -32,6 +34,14 @@ public interface IMonitor : IDisposable
AccessResult SetContrast(int contrast);

AccessResult ChangeTemperature();

int SpeakerVolume { get; }
AccessResult UpdateSpeakerVolume();
AccessResult SetSpeakerVolume(int volume);

bool IsSpeakerMute { get; }
AccessResult UpdateIsSpeakerMute();
AccessResult ToggleSpeakerMute();
}

public enum AccessStatus
Expand All @@ -50,6 +60,8 @@ public class AccessResult
public AccessStatus Status { get; }
public string Message { get; }

public bool IsSuccess => Status == AccessStatus.Succeeded;

public AccessResult(AccessStatus status, string message) => (this.Status, this.Message) = (status, message);

public static readonly AccessResult Succeeded = new(AccessStatus.Succeeded, null);
Expand Down
83 changes: 82 additions & 1 deletion Source/Monitorian.Core/Models/Monitor/MonitorConfiguration.cs
Original file line number Diff line number Diff line change
Expand Up @@ -190,6 +190,7 @@ private enum VcpCode : byte
Contrast = 0x12,
Temperature = 0x14,
SpeakerVolume = 0x62,
AudioMute = 0x8D,
PowerMode = 0xD6,
}

Expand Down Expand Up @@ -270,6 +271,8 @@ private static MonitorCapability GetMonitorCapability(SafePhysicalMonitorHandle
isHighLevelBrightnessSupported: isHighLevelSupported,
isLowLevelBrightnessSupported: vcpCodeValues.ContainsKey((byte)VcpCode.Luminance),
isContrastSupported: vcpCodeValues.ContainsKey((byte)VcpCode.Contrast),
isSpeakerVolumeSupported: vcpCodeValues.ContainsKey((byte)VcpCode.SpeakerVolume),
isSpeakerMuteSupported: vcpCodeValues.ContainsKey((byte)VcpCode.AudioMute),
temperatures: (vcpCodeValues.TryGetValue((byte)VcpCode.Temperature, out byte[] values) ? values : null),
capabilitiesString: (verbose ? capabilitiesString : null),
capabilitiesReport: (verbose ? MakeCapabilitiesReport(vcpCodeValues) : null),
Expand All @@ -279,7 +282,9 @@ private static MonitorCapability GetMonitorCapability(SafePhysicalMonitorHandle
return new MonitorCapability(
isHighLevelBrightnessSupported: isHighLevelSupported,
isLowLevelBrightnessSupported: false,
isContrastSupported: false);
isContrastSupported: false,
isSpeakerVolumeSupported: false,
isSpeakerMuteSupported: false);

static string MakeCapabilitiesReport(IReadOnlyDictionary<byte, byte[]> vcpCodeValues)
{
Expand Down Expand Up @@ -526,6 +531,35 @@ public static (AccessResult result, byte current) GetTemperature(SafePhysicalMon
return (result, (byte)current);
}

/// <summary>
/// Get raw speaker volume.
/// </summary>
/// <param name="physicalMonitorHandle">Physical monitor handle</param>
/// <returns>
/// <para>result: Result</para>
/// <para>minimum: Raw minimum speaker volume (0)</para>
/// <para>current: Raw current speaker volume</para>
/// <para>maximum: Raw maximum speaker volume</para>
/// </returns>
public static (AccessResult result, uint minimum, uint current, uint maximum) GetSpeakerVolume(SafePhysicalMonitorHandle physicalMonitorHandle)
{
return GetVcpValue(physicalMonitorHandle, VcpCode.SpeakerVolume);
}

public static (AccessResult result, bool isMute) IsSpeakerMute(SafePhysicalMonitorHandle physicalMonitorHandle)
{
var (result, _, current, _) = GetVcpValue(physicalMonitorHandle, VcpCode.AudioMute);

// From MCCS Standard:
// Provides for the audio to be muted or unmuted.
// Byte: SL
// 00h Reserved, must be ignored
// 01h Mute the audio
// 02h Unmute the audio
// ≥ 03h Reserved, must be ignored
return (result, current == 1u);
}

private static (AccessResult result, uint minimum, uint current, uint maximum) GetVcpValue(SafePhysicalMonitorHandle physicalMonitorHandle, VcpCode vcpCode)
{
if (!EnsurePhysicalMonitorHandle(physicalMonitorHandle))
Expand Down Expand Up @@ -605,6 +639,37 @@ public static AccessResult SetTemperature(SafePhysicalMonitorHandle physicalMoni
return SetVcpValue(physicalMonitorHandle, VcpCode.Temperature, temperature);
}

/// <summary>
/// Sets raw speaker volume not represented in percentage.
/// </summary>
/// <param name="physicalMonitorHandle">Physical monitor handle</param>
/// <param name="volume">Raw speaker volume (may not always 0 to 100)</param>
/// <returns>Result</returns>
public static AccessResult SetSpeakerVolume(SafePhysicalMonitorHandle physicalMonitorHandle, uint volume)
{
return SetVcpValue(physicalMonitorHandle, VcpCode.SpeakerVolume, volume);
}

/// <summary>
/// Set the audio to be muted or unmuted.
/// </summary>
/// <param name="physicalMonitorHandle">Physical monitor handle</param>
/// <param name="mute"><see langword="true"/> to mute the audio, <see langword="false"/> to unmute</param>
/// <returns>Result</returns>
public static AccessResult ToggleSpeakerMute(SafePhysicalMonitorHandle physicalMonitorHandle, bool mute)
{
// From MCCS Standard:
// Provides for the audio to be muted or unmuted.
// Byte: SL
// 00h Reserved, must be ignored
// 01h Mute the audio
// 02h Unmute the audio
// ≥ 03h Reserved, must be ignored

uint val = mute ? 1u : 2u;
return SetVcpValue(physicalMonitorHandle, VcpCode.AudioMute, val);
}

private static AccessResult SetVcpValue(SafePhysicalMonitorHandle physicalMonitorHandle, VcpCode vcpCode, uint value)
{
if (!EnsurePhysicalMonitorHandle(physicalMonitorHandle))
Expand Down Expand Up @@ -726,6 +791,12 @@ internal class MonitorCapability
[DataMember(Order = 7)]
public string CapabilitiesData { get; }

[DataMember(Order = 8)]
public bool IsSpeakerVolumeSupported { get; }

[DataMember(Order = 9)]
public bool IsSpeakerMuteSupported { get; }

[OnSerializing]
private void OnSerializing(StreamingContext context)
{
Expand All @@ -738,6 +809,8 @@ private void OnSerializing(StreamingContext context)
bool isHighLevelBrightnessSupported,
bool isLowLevelBrightnessSupported,
bool isContrastSupported,
bool isSpeakerVolumeSupported,
bool isSpeakerMuteSupported,
IReadOnlyList<byte> temperatures = null,
string capabilitiesString = null,
string capabilitiesReport = null,
Expand All @@ -746,6 +819,8 @@ private void OnSerializing(StreamingContext context)
isLowLevelBrightnessSupported: isLowLevelBrightnessSupported,
isContrastSupported: isContrastSupported,
isPrecleared: false,
isSpeakerVolumeSupported: isSpeakerVolumeSupported,
isSpeakerMuteSupported: isSpeakerMuteSupported,
temperatures: temperatures,
capabilitiesString: capabilitiesString,
capabilitiesReport: capabilitiesReport,
Expand All @@ -757,6 +832,8 @@ private void OnSerializing(StreamingContext context)
bool isLowLevelBrightnessSupported,
bool isContrastSupported,
bool isPrecleared,
bool isSpeakerVolumeSupported,
bool isSpeakerMuteSupported,
IReadOnlyList<byte> temperatures,
string capabilitiesString,
string capabilitiesReport,
Expand All @@ -766,6 +843,8 @@ private void OnSerializing(StreamingContext context)
this.IsLowLevelBrightnessSupported = isLowLevelBrightnessSupported;
this.IsContrastSupported = isContrastSupported;
this.IsPrecleared = isPrecleared;
this.IsSpeakerVolumeSupported = isSpeakerVolumeSupported;
this.IsSpeakerMuteSupported = isSpeakerMuteSupported;
this.Temperatures = temperatures?.Clip<byte>(3, 10).ToArray(); // 3 is warmest and 10 is coldest.
this.CapabilitiesString = capabilitiesString;
this.CapabilitiesReport = capabilitiesReport;
Expand All @@ -778,6 +857,8 @@ private void OnSerializing(StreamingContext context)
isLowLevelBrightnessSupported: true,
isContrastSupported: true,
isPrecleared: true,
isSpeakerVolumeSupported: false,
isSpeakerMuteSupported: false,
temperatures: null,
capabilitiesString: null,
capabilitiesReport: null,
Expand Down
14 changes: 13 additions & 1 deletion Source/Monitorian.Core/Models/Monitor/MonitorItem.cs
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,8 @@ internal abstract class MonitorItem : IMonitor, IDisposable
public virtual bool IsContrastSupported => false;
public virtual bool IsPrecleared => false;
public virtual bool IsTemperatureSupported => false;
public virtual bool IsSpeakerVolumeSupported => false;
public virtual bool IsSpeakerMuteSupported => false;

public MonitorItem(
string deviceInstanceId,
Expand Down Expand Up @@ -60,6 +62,14 @@ internal abstract class MonitorItem : IMonitor, IDisposable

public virtual AccessResult ChangeTemperature() => AccessResult.NotSupported;

public int SpeakerVolume { get; protected set; } = -1;
public virtual AccessResult UpdateSpeakerVolume() => AccessResult.NotSupported;
public virtual AccessResult SetSpeakerVolume(int volume) => AccessResult.NotSupported;

public bool IsSpeakerMute { get; protected set; } = false;
public virtual AccessResult UpdateIsSpeakerMute() => AccessResult.NotSupported;
public virtual AccessResult ToggleSpeakerMute() => AccessResult.NotSupported;

public override string ToString()
{
return SimpleSerialization.Serialize(
Expand All @@ -77,7 +87,9 @@ public override string ToString()
(nameof(IsTemperatureSupported), IsTemperatureSupported),
(nameof(Brightness), Brightness),
(nameof(BrightnessSystemAdjusted), BrightnessSystemAdjusted),
(nameof(Contrast), Contrast));
(nameof(Contrast), Contrast),
(nameof(SpeakerVolume), SpeakerVolume),
(nameof(IsSpeakerMute), IsSpeakerMute));
}

#region IDisposable
Expand Down
1 change: 1 addition & 0 deletions Source/Monitorian.Core/Monitorian.Core.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@
<Compile Include="Collections\ObservableDictionary.cs" />
<Compile Include="Collections\ObservableKeyedList.cs" />
<Compile Include="Common\BindableBase.cs" />
<Compile Include="Common\RelayCommand.cs" />
<Compile Include="Helper\ArraySearch.cs" />
<Compile Include="Helper\EnumerableExtension.cs" />
<Compile Include="Helper\ExceptionExtension.cs" />
Expand Down
Loading