Skip to content

Instantly share code, notes, and snippets.

@mariodivece
Created July 24, 2017 01:56
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save mariodivece/ef1aa78083508964d97f14c94db401db to your computer and use it in GitHub Desktop.
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.
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