-
Notifications
You must be signed in to change notification settings - Fork 3
/
Copy pathSensor.cs
173 lines (152 loc) · 5.57 KB
/
Sensor.cs
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
using System;
using System.Diagnostics.CodeAnalysis;
using System.Runtime.InteropServices;
using System.Timers;
namespace MediaSensor
{
/// <summary>
/// Reports current media state and raises event when media state changes.
/// </summary>
internal class Sensor : IDisposable
{
/// <summary>
/// Latched value of sensor state
/// </summary>
internal MediaState CurrentState { get; private set; }
/// <summary>
/// Fired when <see cref="CurrentState"/> changes
/// </summary>
internal event EventHandler<SensorStateEventArgs>? StateChanged;
/// <summary>
/// Immediate value of sensor state
/// </summary>
private MediaState TransientState { get; set; }
private Timer SensorTimer { get; set; }
public Timer LatchTimer { get; private set; }
private bool Initialized { get; set; }
internal Sensor()
{
this.SensorTimer = new Timer();
this.LatchTimer = new Timer();
}
internal void Initialize(ConfigurationReader configuration)
{
this.SensorTimer.Interval = configuration.Poll;
this.SensorTimer.Elapsed += OnPollingDelayElapsed;
this.LatchTimer.Interval = configuration.Latch;
this.LatchTimer.AutoReset = false;
this.LatchTimer.Elapsed += OnLatchingDelayElapsed;
this.CurrentState = SensorCore.GetState();
this.Initialized = true;
}
internal void Start()
{
if (this.Initialized)
{
this.SensorTimer.Start();
}
}
internal void Stop()
{
if (this.Initialized)
{
this.SensorTimer.Stop();
this.LatchTimer.Stop();
}
}
public void Dispose()
{
SensorTimer.Stop();
SensorTimer.Elapsed -= OnPollingDelayElapsed;
LatchTimer.Stop();
LatchTimer.Elapsed -= OnLatchingDelayElapsed;
}
private void OnPollingDelayElapsed(object sender, ElapsedEventArgs e)
{
var newState = SensorCore.GetState();
if (newState != TransientState)
{
TransientState = newState;
LatchTimer.Stop(); // Reset the latch timer
LatchTimer.Start();
}
}
private void OnLatchingDelayElapsed(object sender, ElapsedEventArgs e)
{
// If state has changed during latching delay, this timer would have been reset
// and would not have elapsed.
// If we're running this code, it means that the transient value has not changed.
if (CurrentState != TransientState)
{
CurrentState = TransientState;
StateChanged?.Invoke(this, new SensorStateEventArgs(CurrentState));
}
else
{
// We just averted a glitch
}
}
}
/// <summary>
/// Contains code which detects status of the media.
/// Source: https://stackoverflow.com/a/45483843/879243
/// Thank you, Simon Mourier
/// </summary>
internal static class SensorCore
{
public static MediaState GetState()
{
var enumerator = (IMMDeviceEnumerator)(new MMDeviceEnumerator());
var speakers = enumerator.GetDefaultAudioEndpoint(EDataFlow.eRender, ERole.eMultimedia);
var meter = (IAudioMeterInformation)speakers.Activate(typeof(IAudioMeterInformation).GUID, 0, IntPtr.Zero);
var value = meter.GetPeakValue();
// this is a bit tricky. 0 is the official "no sound" value
// but for example, if you open a video and plays/stops with it (w/o killing the app/window/stream),
// the value will not be zero, but something really small (around 1E-09)
// so, depending on your context, it is up to you to decide
// if you want to test for 0 or for a small value
if (value == 0)
return MediaState.Stopped;
else if (value <= 1E-08)
return MediaState.Standby;
else
return MediaState.Playing;
}
[ComImport, Guid("BCDE0395-E52F-467C-8E3D-C4579291692E")]
private class MMDeviceEnumerator
{
}
private enum EDataFlow
{
eRender,
eCapture,
eAll,
}
private enum ERole
{
eConsole,
eMultimedia,
eCommunications,
}
[InterfaceType(ComInterfaceType.InterfaceIsIUnknown), Guid("A95664D2-9614-4F35-A746-DE8DB63617E6")]
private interface IMMDeviceEnumerator
{
void NotNeeded();
IMMDevice GetDefaultAudioEndpoint(EDataFlow dataFlow, ERole role);
// the rest is not defined/needed
}
[InterfaceType(ComInterfaceType.InterfaceIsIUnknown), Guid("D666063F-1587-4E43-81F1-B948E807363F")]
private interface IMMDevice
{
[return: MarshalAs(UnmanagedType.IUnknown)]
object Activate([MarshalAs(UnmanagedType.LPStruct)] Guid iid, int dwClsCtx, IntPtr pActivationParams);
// the rest is not defined/needed
}
[InterfaceType(ComInterfaceType.InterfaceIsIUnknown), Guid("C02216F6-8C67-4B5B-9D00-D008E73E0064")]
private interface IAudioMeterInformation
{
float GetPeakValue();
// the rest is not defined/needed
}
}
}