Click here to Skip to main content
15,884,388 members
Articles / Desktop Programming / MFC

CPianoCtrl - A Display Piano Control

Rate me:
Please Sign up or sign in to vote.
4.80/5 (40 votes)
14 Mar 2008MIT5 min read 209.5K   5K   65  
An article about using the CPianoCtrl class
#if !defined(AFX_PIANOCTRL_H__C84F71CE_FF29_11D6_865D_0030BD08B6D9__INCLUDED_)
#define AFX_PIANOCTRL_H__C84F71CE_FF29_11D6_865D_0030BD08B6D9__INCLUDED_

#if _MSC_VER > 1000
#pragma once
#endif // _MSC_VER > 1000
// PianoCtrl.h : header file
//

/*
    The MIT License

    Copyright (c) 2008 Leslie Sanford

    Permission is hereby granted, free of charge, to any person obtaining a copy
    of this software and associated documentation files (the "Software"), to deal
    in the Software without restriction, including without limitation the rights
    to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
    copies of the Software, and to permit persons to whom the Software is
    furnished to do so, subject to the following conditions:

    The above copyright notice and this permission notice shall be included in
    all copies or substantial portions of the Software.

    THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
    IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
    FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
    AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
    LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
    OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
    THE SOFTWARE.

    Contact: Leslie Sanford (jabberdabber@hotmail.com)

    Last modified: 03/14/2008
*/


//---------------------------------------------------------------------
// Dependencies
//---------------------------------------------------------------------


#include <list>         // For holding the list of CPianoCtrlListeners
#include <vector>       // For holding the CPianoKey objects


//---------------------------------------------------------------------
// CPianoCtrlListener class
//
// This class represents an observer class. It receives notification
// for note on and note off events from the CPianoCtrl class. 
//---------------------------------------------------------------------


// Forward declaration
class CPianoCtrl;


class CPianoCtrlListener
{
public:
    virtual ~CPianoCtrlListener() {}

    virtual void OnNoteOn(CPianoCtrl &PianoCtrl, 
                          unsigned char NoteId) = 0;
    virtual void OnNoteOff(CPianoCtrl &PianoCtrl,
                           unsigned char NoteId) = 0;
};


//---------------------------------------------------------------------
// CPianoCtrl class
//
// This class represents a piano keyboard. 
//---------------------------------------------------------------------
//
// To use the CPianoCtrl class in a dialog based application, use the 
// following steps:
//
//  1. In the resource editor, place a custom control onto the dialog
//     box.
//
//  2. Set the class name in the custom control's property box to match 
//     the CPianoCtrl's class name.
//
//  3. Add a CPianoCtrl instance variable to the dialog class.
//
//  4. Add a DDX_Control call in the DoDataExchange method in the dialog 
//     class. For example, if your dialog class was named CPianoDlg, the 
//     CPianoCtrl variable was named m_PianoCtrl, and the Id for the
//     control was IDC_PIANOCTRL, you would place the following in the
//     dialog class' DoDataExchange function:
//
//      void CPianoDlg::DoDataExchange(CDataExchange* pDX)
//      {
//      	CDialog::DoDataExchange(pDX);
//	        //{{AFX_DATA_MAP(CPianoDlg)
//		    // NOTE: the ClassWizard will add DDX and DDV calls here
//	        //}}AFX_DATA_MAP
//          DDX_Control(pDX, IDC_PIANOCTRL, m_PianoCtrl);
//      }
//
//  5. In the dialog class' OnInitDialog function, initialize the 
//     CPianoCtrl object by calling its Initialize function. Here, you 
//     will pass the desired note range and optionally the note-on 
//     color. 
//
// To use the control dynamically within a dialog box or within a window,
// use the following steps:
//
//  1. Add a CPianoCtrl instance variable to your class. This can be a 
//     pointer to a CPianoCtrl, but if so, you will need to allocate
//     memory for it before using it and deallocate its memory after you
//     are done with it.
//
//  2. Call the CPianoCtrl object's Create function. Here, you will 
//     pass the parent window, the CPianoCtrl's position and size, its
//     Id, and optionally its window style.
//
//  3. Call the CPianoCtrl object's Initialize function. Here, you will 
//     pass the desired note range and optionally the note-on color. 
//
//---------------------------------------------------------------------
//
// Some important notes concerning the CPianoCtrl class:
//
//  
//  It can have up to 128 keys (0 - 127). 
//
//  A CPianoCtrl can be displayed in three ways:
//
//      Horizontal     - Keyboard is displayed horizontally with the 
//                       keys facing down.
//
//      Vertical-left  - Keyboard is displayed vertically with the keys
//                       facing to the right.
//
//      Vertical-right - Keyboard is displayed vertically with the keys
//                       facing to the left.
//
//  The type of display is determined at compile time by passing a 
//  display value to the CPianoCtrl's constructor. Those values are 
//  represented by constants: HORIZONTAL, VERTICAL_LEFT, and 
//  VERTICAL_RIGHT.
//
//  The range is set with the Initialize method or the SetNoteRange
//  method. You specify the lowest note of the range and the highest 
//  note of the range. The lowest note must be less than the highest 
//  note and both the lowest note and the highest note must be natural.
//
//  The number 0 is considered a C note. As you go up from 0, you are 
//  ascending the chromatic scale. Therefore, 0 equals C, 1 equals C#,
//  2 equals D, etc. After you've reached the B note (the first B note 
//  is number 11), the scale starts over from C.
//
//  To be notified of note on and note off events, derive a class from
//  CPianoCtrlListener. Implement its methods in your derived class 
//  and attach an object of that class to a CPianoCtrl object with the
//  AttachListener method. When the CPianoCtrl object receives note on
//  and note off events, it will notify all of its listeners.
//
//---------------------------------------------------------------------  
//
// CPianoCtrl Member Methods
//
//---------------------------------------------------------------------
//
//  Construction
//
//      CPianoCtrl(Orientation Orient = HORIZONTAL)      
//
//          Constructs a CPianoCtrl object. It's important to note that
//          the object has not been fully constructed at this point.
//          A call to the Initialize method is still needed to ready
//          the object for use.
//
//              Orient - The orientation for this control. Can be one of
//                       three values: HORIZONTAL, VERTICAL_LEFT, and
//                       VERTICAL_RIGHT.
//
//      BOOL Create(CWnd *pParentWnd, const RECT &rect, UINT nID, 
//                  DWORD dwStyle = WS_VISIBLE)   
//
//          Creates an CPianoCtrl object dynamically. Returns TRUE if 
//          the operation was successful.
//
//              pParentWnd - Parent window for this control.
//              rect       - Rectangular coordinates for this control.
//              nID        - Identifier for this control.
//              dwStyle    - The window style for this control.   
//
//      BOOL Initialize(unsigned char LowNote, unsigned char HighNote,
//                      COLORREF NoteOnColor = DEF_NOTE_ON_COLOR)
//
//          Initializes this control. Returns true if the operation was
//          successful.
//
//              LowNote     - The lowest note on the keyboard. Must be 
//                            less than the highest note.
//              HighNote    - The highest note on the keyboard. Must be 
//                            less than MAX_NOTE_COUNT.
//              NoteOnColor - The color used to indicate that a key is 
//                            is being played.
//
//---------------------------------------------------------------------
//
//  Operations
//
//      void NoteOn(COLORREF NoteOnColor, unsigned char NoteId)
//      void NoteOn(unsigned char NoteId)
//
//          Turns a note on. 
//
//              NoteOnColor - The color indicating that this key is 
//                            being played.
//              NoteId      - The note to play.       
//
//      void NoteOff(unsigned char NoteId)
//
//          Turns a note off.
//
//              NoteId - The note to stop playing. 
//
//      void AttachListener(CPianoCtrlListener &Listener)
//      void DetachListener(CPianoCtrlListener &Listener)
//
//          Attaches/Detaches CPianoCtrlListener objects. When an 
//          CPianoCtrlListener object is attached to a CPianoCtrl 
//          object, the CPianoCtrl object will notify it when note-on 
//          and note-off events occur.
//
//              Listener - The CPianoCtrlListener to attach/detach.
//
//---------------------------------------------------------------------
//
//  Attributes
//
//      unsigned char GetLowNote() const
//
//          Returns the lowest note for this CPianoCtrl object.      
//
//      unsigned char GetHighNote() const
//
//          Returns the highest note for this CPianoCtrl object.
//
//      BOOL SetNoteRange(unsigned char LowNote, 
//                        unsigned char HighNote)
//      
//          Sets the note range for this CPianoCtrl object. Returns
//          TRUE if the operation was successful.
//
//              LowNote  - The lowest note on the keyboard. Must be 
//                         less than the highest note.
//              HighNote - The highest note on the keyboard. Must be 
//                         less than MAX_NOTE_COUNT.
//
//      COLORREF GetNoteOnColor() const
//
//          Returns the color that indicates a key is being played.
//
//      void SetNoteOnColor(COLORREF NoteOnColor) 
//
//          Sets the color to indicate a key is being played.
//
//              NoteOnColor - The color indicating that a key is 
//                            being played.  
//   
//---------------------------------------------------------------------


// Keyboard orientation
enum Orientation { HORIZONTAL, VERTICAL_LEFT, VERTICAL_RIGHT };


class CPianoCtrl : public CWnd
{
public:
    // Construction/Destruction
    explicit CPianoCtrl(Orientation Orient = HORIZONTAL);
    virtual ~CPianoCtrl();

    // Creates the CPianoCtrl
    BOOL Create(CWnd *pParentWnd, const RECT &rect, UINT nID, 
                DWORD dwStyle = WS_VISIBLE);

    // Initializes the CPianoCtrl - must be called before this 
    // CPianoCtrl object can be used.
    BOOL Initialize(unsigned char LowNote, unsigned char HighNote,
                    COLORREF NoteOnColor = DEF_NOTE_ON_COLOR);

    // Turns note on
    void NoteOn(COLORREF NoteOnColor, unsigned char NoteId);
    void NoteOn(unsigned char NoteId);

    // Turns note off
    void NoteOff(unsigned char NoteId);

    // Attach/Detach CPianoCtrlListener objects
    void AttachListener(CPianoCtrlListener &Listener);
    void DetachListener(CPianoCtrlListener &Listener);

    //
    // Accessors/Mutators
    //

    unsigned char GetLowNote() const { return m_LowNote; }
    unsigned char GetHighNote() const { return m_HighNote; }
    BOOL SetNoteRange(unsigned char LowNote, unsigned char HighNote);

    COLORREF GetNoteOnColor() const { return m_NoteOnColor; }
    void SetNoteOnColor(COLORREF NoteOnColor) 
        { m_NoteOnColor = NoteOnColor; }

// Private functions
private:
    bool IsNoteNatural(unsigned char Note);
    int GetNaturalNoteCount();
    double GetUnitLength();
    int GetUnitCount(double UnitLength);

    void CreatePianoKeys();
    void DestroyPianoKeys();

    int FindKey(CPoint &point);

    // Notify CPianoCtrlListener objects that a note on/off event has
    // occurred.
    void NotifyNoteOn(unsigned char NoteId);
    void NotifyNoteOff(unsigned char NoteId);

    // Register this control's window class
    static void RegisterWindowClass();

    //
    // Copying not allowed
    //

    CPianoCtrl(const CPianoCtrl &PianoCtrl);
    CPianoCtrl &operator = (const CPianoCtrl &PianoCtrl);

// Private classes
private:

    //------------------------------------------------------------------
    // Piano key classes
    //
    // The following classes represent the keys on the CPianoCtrl class. 
    //
    //------------------------------------------------------------------
    //
    // To understand how these classes work, first imagine a piano 
    // keyboard display with a range of 12 keys from C to B:
    //
    //                  ----------------------
    //                  | | || | | | || || | |
    //                  | |_||_| | |_||_||_| |
    //                  |  |  |  |  |  |  |  |
    //                  ----------------------
    //
    // We can divide the keys into 4 types:
    //
    //                  ----------------------
    //                  | | || | | | || || | |
    //                  | |_||_| | |_||_||_| |
    //                  |  |  |  |  |  |  |  |
    //                  ----------------------
    //                  /|\      /|\
    //                   |        | 
    //
    //                  White key left (C and F)
    //
    //
    //                  ----------------------
    //                  | | || | | | || || | |
    //                  | |_||_| | |_||_||_| |
    //                  |  |  |  |  |  |  |  |
    //                  ----------------------
    //                     /|\      /|\ /|\
    //                      |        |   |
    //
    //                White key middle (D, G, and A)
    //
    //
    //                  ----------------------
    //                  | | || | | | || || | |
    //                  | |_||_| | |_||_||_| |
    //                  |  |  |  |  |  |  |  |
    //                  ----------------------
    //                        /|\          /|\ 
    //                         |            |
    //
    //                  White key right (E and B)
    //
    //
    //                     |  |     |  |  |
    //                    \|/\|/   \|/\|/\|/
    //                  ----------------------
    //                  | | || | | | || || | |
    //                  | |_||_| | |_||_||_| |
    //                  |  |  |  |  |  |  |  |
    //                  ----------------------
    //                 
    //            Black key (C#, D#, F#, G#, and A#)
    //
    //
    // There is also a fifth type of key. This is a special key used 
    // when certain ranges are set. For example, look at a range of keys
    // from C to C one octave higher:
    //
    //                  ------------------------
    //                  | | || | | | || || | | | |
    //                  | |_||_| | |_||_||_| | |_|
    //                  |  |  |  |  |  |  |  |  |
    //                  -------------------------
    //
    // A problem occurs because we have half of a black key dangling off
    // the side of the keyboard. This really isn't acceptable, so we 
    // need an additional key to take care of this special case, the
    // white full key:
    //
    //                  -------------------------
    //                  | | || | | | || || | |  |
    //                  | |_||_| | |_||_||_| |  |
    //                  |  |  |  |  |  |  |  |  |
    //                  -------------------------
    //                                        /|\
    //                                         |
    //
    //                        White full key
    //
    // The white full key takes care of those cases in which there would
    // otherwise be a black key chopped in half.
    //
    // The five types of piano keys are each represented by their own 
    // class. Each class knows how to draw itself so that it has the 
    // proper shape on the piano keyboard. All of the classes derive 
    // from one parent class called CPianoKey. This class provides the 
    // interface for all piano key classes.
    //
    // The CPianoCtrl class only allows ranges that begin and end with 
    // natural notes (white keys). This was a judgement call on my part.
    // Ranges in which the client chose a sharp/flat key as the lowest
    // or highest note would create a strange situation in which a white
    // key would be chopped in half:
    //
    //                    --------------------
    //                    | || | | | || || | |
    //                    |_||_| | |_||_||_| |
    //                    ||  |  |  |  |  |  |
    //                    --------------------
    //                   /|\
    //                    |
    //      
    //              Half-white key?
    //
    // This situation could certainly be dealt with, but if the client
    // specified that C# should be the lowest key, they would have to 
    // deal with the extra half-white key that would actually be the 
    // lowest note, in this case C, and not the one they had specified.
    // I decided this situation was more trouble than it was worth, so
    // I limited the low and high end of the range to natural notes.
    //
    //------------------------------------------------------------------
    //
    // The keys are divided into "units." Units are used to determine 
    // how large to draw each key. The length of a unit is represented 
    // by the total length of the control divided by the number of 
    // natural keys in the range divided by units per natural key. 
    //
    // So if the length of the control is 100 and the number of natural
    // keys in the range is 12, and the unit per natural key is 6, 
    // then the unit length would be 1.3888889.
    //
    //-----------------------------------------------------------------


    // Parent piano key class
    class CPianoKey
    {
    public:
        CPianoKey() : m_NoteOnFlag(false) {}
        virtual ~CPianoKey() {}

        // Hit detection
        virtual bool IsPointInKey(const CPoint &pt) const = 0;

        // Turns note on/off
        virtual CRgn *NoteOn(COLORREF NoteOnColor) = 0;
        virtual CRgn *NoteOff() = 0;

        // Paints this key
        virtual void Paint(CDC *dc) = 0;

    public:

        // Units per natural key
        static const int UNIT_PER_NAT_KEY;

        // Units per flat key
        static const int UNIT_PER_FLAT_KEY;

        // Determines black keys' width
        static const double BLACK_KEY_OFFSET;

    protected:
        void Paint(CDC *dc, CRgn *Rgn, COLORREF NoteOffColor);

    protected:
        bool m_NoteOnFlag;
        COLORREF m_NoteOnColor;
    };


    // CWhiteKeyLeft class
    class CWhiteKeyLeft : public CPianoKey
    {
    public:
        CWhiteKeyLeft(Orientation Orient, double UnitLength, int Width, 
                      double Position);
        ~CWhiteKeyLeft()
        { m_Rgn.DeleteObject(); }

        bool IsPointInKey(const CPoint &pt) const;

        CRgn *NoteOn(COLORREF NoteOnColor);
        CRgn *NoteOff();

        void Paint(CDC *dc);

    private:
        // Initializes the region representing this key
        void InitHorRgn(double UnitLength, int Width, double Position);
        void InitVerLeftRgn(double UnitLength, int Width, double Position);
        void InitVerRightRgn(double UnitLength, int Width, double Position);

    private:
        CRgn m_Rgn;
        POINT Points[6];
    };


    // CWhiteKeyMiddle class
    class CWhiteKeyMiddle : public CPianoKey
    {
    public:
        CWhiteKeyMiddle(Orientation Orient, double UnitLength, int Width, 
                        double Position);
        ~CWhiteKeyMiddle()
        { m_Rgn.DeleteObject(); }

        bool IsPointInKey(const CPoint &pt) const;

        CRgn *NoteOn(COLORREF NoteOnColor);
        CRgn *NoteOff();

        void Paint(CDC *dc);

    private:
        // Initializes the region representing this key
        void InitHorRgn(double UnitLength, int Width, double Position);
        void InitVerLeftRgn(double UnitLength, int Width, double Position);
        void InitVerRightRgn(double UnitLength, int Width, double Position);

    private:
        CRgn m_Rgn;
        POINT Points[8];
    };


    // CWhiteKeyRight class
    class CWhiteKeyRight : public CPianoKey
    {
    public:
        CWhiteKeyRight(Orientation Orient, double UnitLength, int Width, 
                       double Position);
        ~CWhiteKeyRight()
        { m_Rgn.DeleteObject(); }

        bool IsPointInKey(const CPoint &pt) const;

        CRgn *NoteOn(COLORREF NoteOnColor);
        CRgn *NoteOff();

        void Paint(CDC *dc);

    private:
        // Initializes the region representing this key
        void InitHorRgn(double UnitLength, int Width, double Position);
        void InitVerLeftRgn(double UnitLength, int Width, double Position);
        void InitVerRightRgn(double UnitLength, int Width, double Position);

    private:
        CRgn m_Rgn;
        POINT Points[6];
    };


    // CWhiteKeyFull class
    class CWhiteKeyFull : public CPianoKey
    {
    public:
        CWhiteKeyFull(Orientation Orient, double UnitLength, int Width, 
                      double Position);
        ~CWhiteKeyFull()
        { m_Rgn.DeleteObject(); }

        bool IsPointInKey(const CPoint &pt) const;

        CRgn *NoteOn(COLORREF NoteOnColor);
        CRgn *NoteOff();

        void Paint(CDC *dc);

    private:
        // Initializes the region representing this key
        void InitHorRgn(double UnitLength, int Width, double Position);
        void InitVerLeftRgn(double UnitLength, int Width, double Position);
        void InitVerRightRgn(double UnitLength, int Width, double Position);

    private:
        CRgn m_Rgn;
        POINT Points[4];
    };


    // CBlackKey class
    class CBlackKey : public CPianoKey
    {
    public:
        CBlackKey(Orientation Orient, double UnitLength, int Width, 
                  double Position);
        ~CBlackKey()
        { m_Rgn.DeleteObject(); }

        bool IsPointInKey(const CPoint &pt) const;

        CRgn *NoteOn(COLORREF NoteOnColor);
        CRgn *NoteOff();

        void Paint(CDC *dc);

    private:
        // Initializes the region representing this key
        void InitHorRgn(double UnitLength, int Width, double Position);
        void InitVerLeftRgn(double UnitLength, int Width, double Position);
        void InitVerRightRgn(double UnitLength, int Width, double Position);

    private:
        CRgn m_Rgn;
        POINT Points[4];
    };



// Public attributes
public:
    // The class name of this control
    static const char CLASS_NAME[];

    // Default color used for indicating that a note is being played
    static const COLORREF DEF_NOTE_ON_COLOR;

    // The maximum number of notes this piano control can have
    static const int MAX_NOTE_COUNT;

    // Note identifiers
    enum NoteId { C, C_SHARP, D, D_SHARP, E, F, F_SHARP, G, G_SHARP, A, 
                  A_SHARP, B };

    // Table of note identifiers
    static const int NOTE_TABLE[];

// Private attributes
private:
    // For protecting access to the list of listeners
    CRITICAL_SECTION m_CriticalSection;

    // Color indicating a key is being played
    COLORREF m_NoteOnColor;

    // Keyboard orientation
    Orientation m_Orientation;

    // Rectangular area of this control
    CRect m_ClientRect;

    // Lenght of this control
    int m_Length;

    // Width of this control
    int m_Width;            
    
    // Low note of the range
    unsigned char m_LowNote;

    // High note of the range
    unsigned char m_HighNote; 

    // Flag for indicating whether or not this object has been 
    // intialized.
    bool m_IsInitialized;

    // Flag for indicating whether or not this window has captured the 
    // mouse. 
    bool m_HasCapture;    

    // Collection of piano key objects
    std::vector<CPianoKey *> m_Keys;

    // The current active key (triggered by the mouse)
    int m_CurrKey;

    // Collection of piano control listeners
    std::list<CPianoCtrlListener *> m_Listeners; 

    // Flag for whether or note this custom control's Window class has 
    // been registered.
    static bool REGISTER_FLAG;

    // Messages for range errors
    static const CString NOTE_RANGE_ERR;
    static const CString LOW_NOTE_RANGE_ERR;
    static const CString HIGH_NOTE_RANGE_ERR;
    static const CString INVALID_RANGE_ERR;
    static const CString NATURAL_NOTE_ERR;


//----------------------------------------------------------------------
// Wizard generated code 
//----------------------------------------------------------------------


public:

// Overrides
	// ClassWizard generated virtual function overrides
	//{{AFX_VIRTUAL(CPianoCtrl)
	//}}AFX_VIRTUAL

	// Generated message map functions
protected:
	//{{AFX_MSG(CPianoCtrl)
	afx_msg void OnPaint();
	afx_msg void OnLButtonDown(UINT nFlags, CPoint point);
	afx_msg void OnLButtonUp(UINT nFlags, CPoint point);
	afx_msg void OnMouseMove(UINT nFlags, CPoint point);
	//}}AFX_MSG
	DECLARE_MESSAGE_MAP()
};

/////////////////////////////////////////////////////////////////////////////

//{{AFX_INSERT_LOCATION}}
// Microsoft Visual C++ will insert additional declarations immediately before the previous line.

#endif // !defined(AFX_PIANOCTRL_H__C84F71CE_FF29_11D6_865D_0030BD08B6D9__INCLUDED_)

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