Click here to Skip to main content
15,885,985 members
Articles / Programming Languages / C#

C# MIDI Toolkit

Rate me:
Please Sign up or sign in to vote.
4.95/5 (177 votes)
18 Apr 2007MIT18 min read 3.2M   41.8K   303  
A toolkit for creating MIDI applications with C#.
/*
 * Created by: Leslie Sanford
 * 
 * Contact: jabberdabber@hotmail.com
 * 
 * Last modified: 05/07/2003
 */


using System;
using System.Reflection;
using System.Resources;
using System.Runtime.InteropServices;
using System.Threading;

namespace Multimedia
{
    /// <summary>
    /// Specifies constants for multimedia timer event types.
    /// </summary>
    public enum TimerMode 
    { 
        /// <summary>
        /// Timer event occurs once.
        /// </summary>
        OneShot, 
        
        /// <summary>
        /// Timer event occurs periodically.
        /// </summary>
        Periodic 
    };

    /// <summary>
    /// Represents the method that handles calls from a multimedia timer.
    /// </summary>
    /// <param name="state">
    /// An object containing application-specific information relevant to the 
    /// method invoked by this delegate, or a null reference.
    /// </param>
    /// <remarks>
    /// Use a TimerCallback delegate to specify the method that is called by a 
    /// multimedia timer.
    /// </remarks>
    public delegate void TimerCallback(object state);

    /// <summary>
    /// Represents information about the timer's capabilities.
    /// </summary>
    [StructLayout(LayoutKind.Sequential)]
    public struct TimerCaps
    {
        /// <summary>
        /// Minimum supported period.
        /// </summary>
        public int periodMin;

        /// <summary>
        /// Maximum supported period.
        /// </summary>
        public int periodMax;
    }

    /// <summary>
    /// Represents a multimedia timer.
    /// </summary>
    /// <remarks>
    /// <para>The multimedia timer allows applications to schedule timer events 
    /// with the greatest resolution (or accuracy) possible for the hardware 
    /// platform. The multimedia timer allows you to schedule timer events at a 
    /// higher resolution than other timer services.</para>
    /// 
    /// <para>The multimedia timer is useful for applications that demand 
    /// high-resolution timing. For example, a Midi sequencer requires a 
    /// high-resolution timer because it must maintain the pace of Midi events 
    /// within a resolution of 1 millisecond.</para>
    /// </remarks>
    public sealed class Timer : IDisposable
    {
        #region Timer Members

        #region Delegates

        // Represents the method that is called by Windows when a timer event
        // occurs.
        private delegate void TimeProc(int id, int msg, int user, int param1, 
            int param2);

        #endregion

        #region Win32 Multimedia Timer Functions

        // Gets timer capabilities.
        [DllImport("winmm.dll")]
        private static extern int timeGetDevCaps(ref TimerCaps caps, 
            int sizeOfTimerCaps);

        // Creates and starts the timer.
        [DllImport("winmm.dll")]
        private static extern int timeSetEvent(int delay, int resolution, 
            TimeProc proc, int user, int mode);

        // Stops and destroys the timer.
        [DllImport("winmm.dll")]
        private static extern int timeKillEvent(int id);      
        
        #endregion

        #region Fields

        // Timer identifier.
        private int timerId;

        // Timer mode.
        private TimerMode mode;

        // Period between timer events in milliseconds.
        private int period;

        // Timer resolution in milliseconds.
        private int resolution;

        // Indicates whether or not the timer is running.
        private bool running;

        // Called by Windows when a timer periodic event occurs.
        private TimeProc timeProcPeriodic;

        // Called by Windows when a timer one shot event occurs.
        private TimeProc timeProcOneShot;

        // User supplied callback that is called when a timer event occurs.
        private TimerCallback callback;

        // User supplied state information.
        private object state; 

        // Multimedia timer capabilities.
        private static TimerCaps caps;

        // Resource manager - gets error messages for exceptions.
        private static ResourceManager resManager = new 
            ResourceManager("Multimedia.Resource", 
            Assembly.GetExecutingAssembly());

        #endregion

        #region Construction/Destruction

        /// <summary>
        /// Initialize class.
        /// </summary>
        static Timer()
        {
            // Get multimedia timer capabilities.
            timeGetDevCaps(ref caps, Marshal.SizeOf(caps));            
        }

        /// <summary>
        /// Initializes a new instance of the Timer class with the user 
        /// supplied callback and state information, timer period, and 
        /// resolution.
        /// </summary>
        /// <param name="callback">
        /// A TimerCallback delegate representing a method to be executed when 
        /// a timer event occurs. 
        /// </param>
        /// <param name="state">
        /// An object containing information to be used by the callback method, 
        /// or a null reference.
        /// </param>
        /// <param name="period">
        /// The time between timer events in milliseconds.
        /// </param>
        /// <param name="resolution">
        /// The timer resolution in milliseconds.
        /// </param>
        public Timer(TimerCallback callback, object state, int period, 
            int resolution)
        {
            // 
            // Initialize fields.
            //

            this.callback = callback;
            this.state = state;
            timeProcPeriodic = new TimeProc(OnTimerPeriodicEvent);
            timeProcOneShot = new TimeProc(OnTimerOneShotEvent);
            Mode = TimerMode.Periodic;
            Period = period;
            Resolution = resolution;
            running = false;
        }

        /// <summary>
        /// Initializes a new instance of the Timer class with the user 
        /// supplied callback and state information, timer period, resolution,
        /// and timer mode.
        /// </summary>
        /// <param name="callback">
        /// A TimerCallback delegate representing a method to be executed when 
        /// a timer event occurs. 
        /// </param>
        /// <param name="state">
        /// An object containing information to be used by the callback method, 
        /// or a null reference.
        /// </param>
        /// <param name="period">
        /// The time between timer events in milliseconds.
        /// </param>
        /// <param name="resolution">
        /// The timer resolution in milliseconds.
        /// </param>
        /// <param name="mode">
        /// The timer mode.
        /// </param>
        public Timer(TimerCallback callback, object state, int period, 
            int resolution, TimerMode mode)
        {
            // 
            // Initialize fields.
            //

            this.callback = callback;
            this.state = state;
            timeProcPeriodic = new TimeProc(OnTimerPeriodicEvent);
            timeProcOneShot = new TimeProc(OnTimerOneShotEvent);
            Mode = mode;
            Period = period;
            Resolution = resolution;
            running = false;            
        }

        /// <summary>
        /// Destructor.
        /// </summary>
        ~Timer()
        {
            if(running)
            {
                Stop();
            }
        }

        #endregion

        #region Methods

        /// <summary>
        /// Starts the timer.
        /// </summary>
        /// <exception cref="TimerStartException">
        /// Thrown if unable to start timer.
        /// </exception>
        public void Start()
        {
            // If the timer is already running.
            if(running)
            {
                // Stop timer.
                Stop();
            }

            // If the periodic event callback should be used.
            if(mode == TimerMode.Periodic)
            {
                // Create and start timer.
                timerId = timeSetEvent(Period, Resolution, timeProcPeriodic, 0, 
                    (int)Mode);
            }
            // Else the one shot event callback should be used.
            else
            {
                // Create and start timer.
                timerId = timeSetEvent(Period, Resolution, timeProcOneShot, 0, 
                    (int)Mode);
            }

            // If the timer was created successfully.
            if(timerId != 0)
            {
                // Indicate that the timer is now running.
                running = true;
            }
            // Else an error occurred. 
            else
            {
                // Get error message and throw exception.
                string msg = resManager.GetString("TimerStartFailed");
                throw new TimerStartException(msg);
            }
        }

        /// <summary>
        /// Stops timer.
        /// </summary>
        public void Stop()
        {
            // If the timer is running.
            if(running)
            {
                // Stop and destroy timer.
                timeKillEvent(timerId);

                // Indicate that the timer is not running.
                running = false;
            }
        }

        /// <summary>
        /// Indicates whether or not the timer is running.
        /// </summary>
        /// <returns>
        /// Returns true if the timer is running; otherwise, false.
        /// </returns>
        public bool IsRunning()
        {
            return running;
        }

        /// <summary>
        /// Callback method called by the Win32 multimedia timer when a timer
        /// periodic event occurs.
        /// </summary>
        private void OnTimerPeriodicEvent(int id, int msg, int user, 
            int param1, int param2)
        {
            // Call user supplied callback.
            callback(state);
        }

        /// <summary>
        /// Callback method called by the Win32 multimedia timer when a timer
        /// one shot event occurs.
        /// </summary>
        private void OnTimerOneShotEvent(int id, int msg, int user, int param1, 
            int param2)
        {
            // Call user supplied callback.
            callback(state);

            // Stop timer.
            Stop();
        }

        #endregion

        #region Properties

        /// <summary>
        /// Gets or sets the timer mode.
        /// </summary>
        /// <remarks>
        /// If the timer is running when the mode is changed, the timer is 
        /// stopped and restarted with the new mode value.
        /// </remarks>
        public TimerMode Mode
        {
            get
            {
                return mode;
            }
            set
            {
                mode = value;

                // If the timer is running.
                if(IsRunning())
                {
                    // Stop and restart timer.
                    Stop();
                    Start();
                }
            }
        }

        /// <summary>
        /// Gets or sets the timer period.
        /// </summary>
        /// <exception cref="ArgumentOutOfRangeException">
        /// Thrown if the period is set to a value out of range for the 
        /// multimedia timer.
        /// </exception>
        /// <remarks>
        /// <para>The multimedia timer period is the time between timer events 
        /// in milliseconds.</para>
        /// 
        /// <para>If the timer is running when the period is changed, the timer 
        /// is stopped and restarted with the new period value.</para>   
        /// </remarks>     
        public int Period
        {
            get
            {
                return period;
            }
            set
            {
                // If period is in range.
                if(value >= caps.periodMin && value <= caps.periodMax)
                {
                    // Assign period.
                    period = value;

                    // If the timer is running.
                    if(IsRunning())
                    {
                        // Stop and restart timer.
                        Stop();
                        Start();
                    }
                }
                // Else period is out of range.
                else
                {
                    // Stop timer before throwing exception (has no effect if
                    // the timer is not running).
                    Stop();

                    // Get error message and throw exception.
                    string msg = resManager.GetString("TimerPeriodOutOfRange");
                    throw new ArgumentOutOfRangeException("Period", value, msg);
                }
            }
        }

        /// <summary>
        /// Gets or sets the timer resolution.
        /// </summary>
        /// <exception cref="ArgumentOutOfRangeException">
        /// Thrown if the resolution is set to a value less than zero.
        /// </exception>
        /// <remarks>
        /// <para>The resolution is in milliseconds. The resolution increases 
        /// with smaller values; a resolution of 0 indicates periodic events 
        /// should occur with the greatest possible accuracy. To reduce system 
        /// overhead, however, you should use the maximum value appropriate 
        /// for your application.</para>
        /// 
        /// <para>If the timer is running when the resolution is changed, the 
        /// timer is stopped and restarted with the new resolution value.
        /// </para>        
        /// </remarks>
        public int Resolution
        {
            get
            {
                return resolution;
            }
            set
            {
                // If resolution is in range.
                if(value >= 0)
                {
                    // Assign resolution.
                    resolution = value;

                    // If the timer is running.
                    if(IsRunning())
                    {
                        // Stop and restart timer.
                        Stop();
                        Start();
                    }
                }
                // Else resolution is out of range.
                else
                {
                    // Stop timer before throwing exception (has no effect if
                    // the timer is not running).
                    Stop();

                    // Get error message and throw exception.
                    string msg = resManager.GetString("TimerResolutionOutOfRange");
                    throw new ArgumentOutOfRangeException("Resolution", value, msg);
                }
            }
        }

        /// <summary>
        /// Gets the timer capabilities.
        /// </summary>
        public static TimerCaps Capabilities
        {
            get
            {
                return caps;
            }
        }

        #endregion

        #endregion

        #region IDisposable Members

        /// <summary>
        /// Frees timer resources.
        /// </summary>
        public void Dispose()
        {
            // If the timer is still running.
            if(running)
            {
                // Stop timer.
                Stop();
            }
        }

        #endregion
    }

    /// <summary>
    /// The exception that is thrown when a timer fails to start.
    /// </summary>
    public class TimerStartException : ApplicationException
    {
        /// <summary>
        /// Initializes a new instance of the TimerStartException class.
        /// </summary>
        public TimerStartException()
        {
        }

        /// <summary>
        /// Initializes a new instance of the TimerStartException class.
        /// </summary>
        /// <param name="message">
        /// The error message that explains the reason for the exception. 
        /// </param>
        public TimerStartException(string message) : base(message)
        {
        }
    }
}

By viewing downloads associated with this article you agree to the Terms of Service and the article's licence.

If a file you wish to view isn't highlighted, and is a text file (not binary), please let us know and we'll add colourisation support for it.

License

This article, along with any associated source code and files, is licensed under The MIT License


Written By
United States United States
Aside from dabbling in BASIC on his old Atari 1040ST years ago, Leslie's programming experience didn't really begin until he discovered the Internet in the late 90s. There he found a treasure trove of information about two of his favorite interests: MIDI and sound synthesis.

After spending a good deal of time calculating formulas he found on the Internet for creating new sounds by hand, he decided that an easier way would be to program the computer to do the work for him. This led him to learn C. He discovered that beyond using programming as a tool for synthesizing sound, he loved programming in and of itself.

Eventually he taught himself C++ and C#, and along the way he immersed himself in the ideas of object oriented programming. Like many of us, he gotten bitten by the design patterns bug and a copy of GOF is never far from his hands.

Now his primary interest is in creating a complete MIDI toolkit using the C# language. He hopes to create something that will become an indispensable tool for those wanting to write MIDI applications for the .NET framework.

Besides programming, his other interests are photography and playing his Les Paul guitars.

Comments and Discussions