Created
July 24, 2017 01:56
-
-
Save mariodivece/ef1aa78083508964d97f14c94db401db to your computer and use it in GitHub Desktop.
A timer based on the multimedia timer API with approximately 1 millisecond precision.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
namespace Unosquare.FFME.Core | |
{ | |
using System; | |
using System.ComponentModel; | |
using System.Runtime.InteropServices; | |
using System.Threading; | |
using System.Threading.Tasks; | |
/// <summary> | |
/// A timer based on the multimedia timer API with approximately 1 millisecond precision. | |
/// </summary> | |
public sealed class MultimediaTimer : IDisposable | |
{ | |
#region Constant Declarations | |
private const int EventTypeSingle = 0; | |
private const int EventTypePeriodic = 1; | |
private static readonly Task TaskDone = Task.FromResult<object>(null); | |
#endregion | |
#region State Variables | |
private bool HasDisposed = default(bool); | |
private volatile uint TimerId = default(uint); | |
private int m_Interval = default(int); | |
private int m_Resolution = default(int); | |
/// <summary> | |
/// Hold the timer callback to prevent garbage collection. | |
/// </summary> | |
private readonly NativeMethods.MultimediaTimerCallback Callback; | |
#endregion | |
#region Event Handlers | |
/// <summary> | |
/// Occurs when the timer interval is hit. | |
/// Since Windows is not a real-time OS, the load on your system may cause the MM timer be delayed | |
/// resulting in gaps , for example of of 100 ms that contain 100 events in quick succession, | |
/// rather than 100 events spaced 1 ms apart. | |
/// </summary> | |
public event EventHandler Elapsed; | |
#endregion | |
#region Constructors | |
/// <summary> | |
/// Initializes a new instance of the <see cref="MultimediaTimer"/> class. | |
/// The resolution is set to 5ms and the interval to 10ms. | |
/// Resultion | |
/// </summary> | |
public MultimediaTimer() | |
{ | |
Callback = new NativeMethods.MultimediaTimerCallback(TimerCallbackMethod); | |
Resolution = 5; | |
Interval = 10; | |
} | |
#endregion | |
#region Properties | |
/// <summary> | |
/// The period of the timer in milliseconds. | |
/// Set this value before calling the Start method | |
/// </summary> | |
public int Interval | |
{ | |
get | |
{ | |
return m_Interval; | |
} | |
set | |
{ | |
CheckDisposed(); | |
if (IsRunning) | |
throw new InvalidOperationException($"{nameof(MultimediaTimer)} is already running."); | |
if (value < 0) | |
throw new ArgumentOutOfRangeException(nameof(value)); | |
m_Interval = value; | |
if (Resolution > Interval) | |
Resolution = value; | |
} | |
} | |
/// <summary> | |
/// The resolution of the timer in milliseconds. | |
/// The minimum resolution is 0, meaning highest possible resolution. | |
/// Set this value before starting the timer. | |
/// </summary> | |
public int Resolution | |
{ | |
get { return m_Resolution; } | |
set | |
{ | |
CheckDisposed(); | |
if (value < 0) | |
throw new ArgumentOutOfRangeException(nameof(value)); | |
m_Resolution = value; | |
} | |
} | |
/// <summary> | |
/// Gets whether the timer has been started. | |
/// </summary> | |
public bool IsRunning | |
{ | |
get { return TimerId != 0; } | |
} | |
#endregion | |
#region Public API | |
/// <summary> | |
/// Starts the timer. | |
/// </summary> | |
/// <exception cref="InvalidOperationException">MultimediaTimer</exception> | |
/// <exception cref="Win32Exception"></exception> | |
public void Start() | |
{ | |
CheckDisposed(); | |
if (IsRunning) | |
throw new InvalidOperationException($"{nameof(MultimediaTimer)} is already running."); | |
// Event type = 0, one off event | |
// Event type = 1, periodic event | |
var userContext = default(uint); | |
TimerId = NativeMethods.TimeSetEvent((uint)Interval, (uint)Resolution, Callback, ref userContext, EventTypePeriodic); | |
if (TimerId == 0) | |
{ | |
var error = Marshal.GetLastWin32Error(); | |
throw new Win32Exception(error); | |
} | |
} | |
/// <summary> | |
/// Stops the timer. | |
/// </summary> | |
/// <exception cref="InvalidOperationException">MultimediaTimer</exception> | |
public void Stop() | |
{ | |
CheckDisposed(); | |
if (IsRunning == false) | |
throw new InvalidOperationException($"{nameof(MultimediaTimer)} has not been started."); | |
StopInternal(); | |
} | |
#endregion | |
#region Static Methods | |
public static Task Delay(int millisecondsDelay, CancellationToken token) | |
{ | |
return CreateDelay(millisecondsDelay, token); | |
} | |
public static Task Delay(int millisecondsDelay) | |
{ | |
return CreateDelay(millisecondsDelay, default(CancellationToken)); | |
} | |
private static Task CreateDelay(int millisecondsDelay, CancellationToken token) | |
{ | |
if (millisecondsDelay < 0) | |
{ | |
throw new ArgumentOutOfRangeException( | |
nameof(millisecondsDelay), millisecondsDelay, "The value cannot be less than 0."); | |
} | |
if (millisecondsDelay == 0) | |
{ | |
return TaskDone; | |
} | |
token.ThrowIfCancellationRequested(); | |
// allocate an object to hold the callback in the async state. | |
var state = new object[1]; | |
var completionSource = new TaskCompletionSource<object>(state); | |
NativeMethods.MultimediaTimerCallback callback = (uint id, uint message, ref uint context, uint reserved1, uint reserved2) => | |
{ | |
// Note we don't need to kill the timer for one-off events. | |
completionSource.TrySetResult(null); | |
}; | |
state[0] = callback; | |
uint userContext = 0; | |
var timerId = NativeMethods.TimeSetEvent((uint)millisecondsDelay, (uint)0, callback, ref userContext, EventTypeSingle); | |
if (timerId == 0) | |
{ | |
var error = Marshal.GetLastWin32Error(); | |
throw new Win32Exception(error); | |
} | |
return completionSource.Task; | |
} | |
#endregion | |
#region Private Methods | |
/// <summary> | |
/// Internal Stop method. | |
/// </summary> | |
private void StopInternal() | |
{ | |
NativeMethods.TimeKillEvent(TimerId); | |
TimerId = 0; | |
} | |
/// <summary> | |
/// Invokes the event handler. | |
/// </summary> | |
private void TimerCallbackMethod(uint id, uint message, ref uint userContext, uint reserved1, uint reserved2) | |
{ | |
Elapsed?.Invoke(this, EventArgs.Empty); | |
} | |
#endregion | |
#region IDisposable Support | |
/// <summary> | |
/// Checks if this instance has been disposed. | |
/// </summary> | |
/// <exception cref="ObjectDisposedException">MultimediaTimer</exception> | |
private void CheckDisposed() | |
{ | |
if (HasDisposed) | |
throw new ObjectDisposedException(nameof(MultimediaTimer)); | |
} | |
/// <summary> | |
/// Releases unmanaged and - optionally - managed resources. | |
/// </summary> | |
/// <param name="alsoManaged"><c>true</c> to release both managed and unmanaged resources; <c>false</c> to release only unmanaged resources.</param> | |
private void Dispose(bool alsoManaged) | |
{ | |
if (HasDisposed) | |
return; | |
HasDisposed = true; | |
if (IsRunning) | |
{ | |
StopInternal(); | |
} | |
if (alsoManaged) | |
{ | |
Elapsed = null; | |
GC.SuppressFinalize(this); | |
} | |
} | |
/// <summary> | |
/// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources. | |
/// </summary> | |
public void Dispose() | |
{ | |
Dispose(true); | |
} | |
/// <summary> | |
/// Finalizes an instance of the <see cref="MultimediaTimer"/> class. | |
/// </summary> | |
~MultimediaTimer() | |
{ | |
Dispose(false); | |
} | |
#endregion | |
#region Native APIs | |
/// <summary> | |
/// Provided native Multimedia Timer Methods | |
/// </summary> | |
private static class NativeMethods | |
{ | |
const string WinMM = "winmm.dll"; | |
public delegate void MultimediaTimerCallback(uint id, uint message, ref uint usertContext, uint reserved1, uint reserved2); | |
[DllImport(WinMM, SetLastError = true, EntryPoint = "timeSetEvent")] | |
public static extern uint TimeSetEvent(uint msDelay, uint msResolution, MultimediaTimerCallback callback, ref uint userCtx, uint eventType); | |
[DllImport(WinMM, SetLastError = true, EntryPoint = "timeKillEvent")] | |
public static extern void TimeKillEvent(uint uTimerId); | |
} | |
#endregion | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment