Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles / Languages / C#

DryWetMIDI: Working with MIDI Devices

5.00/5 (7 votes)
27 Jun 2021CPOL13 min read 36.5K  
Overview of how to send, receive, play back and record MIDI data with DryWetMIDI

Introduction

DryWetMIDI is .NET library to work with MIDI files and MIDI devices. To learn about processing MIDI files, read "DryWetMIDI: High-level processing of MIDI files".

Devices API provided by the DryWetMIDI is the subject of this article. It shows how to send MIDI events to and to receive them from a MIDI device. Also, playing and recording MIDI data are described.

Please note that this article doesn't cover all available API. Please read the library documentation to learn more.

[^] top

Contents

  1. API Overview
  2. InputDevice
  3. OutputDevice
  4. DevicesConnector
  5. Playback
  6. Recording
  7. VirtualDevice
  8. DevicesWatcher
  9. Links
  10. History

[^] top

API Overview

DryWetMIDI provides the ability to send MIDI data to or receive it from a MIDI device. For that purpose, there are following types:

For macOS following two classes are also available:

  • VirtualDevice
  • DevicesWatcher

DryWetMIDI provides built-in implementations of IInputDevice and IOutputDeviceInputDevice and OutputDevice correspondingly. Talking about devices we will use these classes instead of interfaces. These types implement IDisposable and you should always dispose them to free devices for using by other applications. You can read more details about MIDI devices API by following the links above.

Also there are two important classes to play MIDI data back and capture MIDI data from a device:

MIDI devices API classes are placed in the Melanchall.DryWetMidi.Multimedia namespace.

To understand what is input and output device in DryWetMIDI, take a look at the following image:

Image 1

So, as you can see, although a MIDI port is MIDI IN for hardware device, it will be an output device (OutputDevice) in DryWetMIDI because your application will send MIDI data to this port. MIDI OUT of hardware device will be an input device (InputDevice) in DryWetMIDI because a program will receive MIDI data from the port.

InputDevice and OutputDevice are derived from MidiDevice class which has the following public members:

C#
public abstract class MidiDevice : IDisposable
{
    // ...
    public event EventHandler<ErrorOccurredEventArgs> ErrorOccurred;
    // ...
    public string Name { get; }
    // ...
}

If some error occurred while sending or receiving a MIDI event, the ErrorOccurred event will be fired holding an exception caused the error.

[^] top

InputDevice

In DryWetMIDI, an input MIDI device is represented by InputDevice class. It allows to receive events from a MIDI device.

To get an instance of InputDevice, you can use either GetByName or GetByIndex static methods. Index of a MIDI device is a number from 0 to devices count minus one. To retrieve count of input MIDI devices presented in the system, there is the GetDevicesCount method. You can get all input MIDI devices with GetAll method.

After an instance of InputDevice is obtained, call StartEventsListening to start listening to incoming MIDI events going from an input MIDI device. If you don't need to listen for events anymore, call StopEventsListening. To check whether InputDevice is currently listening for events, use IsListeningForEvents property.

If an input device is listening for events, it will fire EventReceived event for each incoming MIDI event. Args of the event hold a MidiEvent received.

See API overview section for common members of a MIDI device class that are inherited by InputDevice from the base class MidiDevice.

Small example that shows receiving MIDI data:

C#
using System;
using Melanchall.DryWetMidi.Multimedia;

// ...

using (var inputDevice = InputDevice.GetByName("Some MIDI device"))
{
    inputDevice.EventReceived += OnEventReceived;
    inputDevice.StartEventsListening();
}

// ...

private void OnEventReceived(object sender, MidiEventReceivedEventArgs e)
{
    var midiDevice = (MidiDevice)sender;
    Console.WriteLine($"Event received from '{midiDevice.Name}' at {DateTime.Now}: {e.Event}");
}

Note that you should always take care about disposing an InputDevice, i.e., use it inside using block or call Dispose manually. Without it, all resources taken by the device will live until GC collects them via finalizer of the InputDevice. It means that sometimes, you will not be able to use different instances of the same device across multiple applications or different pieces of a program.

By default, InputDevice will fire MidiTimeCodeReceived event when all MIDI Time Code components (MidiTimeCodeEvent events) are received forming hours:minutes:seconds:frames timestamp. You can turn this behavior off by setting RaiseMidiTimeCodeReceived to false.

If an invalid event received, ErrorOccurred event will be fired holding the data of the invalid event.

[^] top

OutputDevice

In DryWetMIDI, an output MIDI device is represented by OutputDevice class. It allows to send events to a MIDI device.

To get an instance of OutputDevice, you can use either GetByName or GetByIndex static methods. Index of a MIDI device is a number from 0 to devices count minus one. To retrieve count of output MIDI devices presented in the system, there is the GetDevicesCount method. You can get all output MIDI devices with GetAll method:

C#
using System;
using Melanchall.DryWetMidi.Multimedia;

// ...

foreach (var outputDevice in OutputDevice.GetAll())
{
    Console.WriteLine(outputDevice.Name);
}

After an instance of OutputDevice is obtained, you can send MIDI events to device via SendEvent method. You cannot send meta events since such events can be inside a MIDI file only. If you pass an instance of meta event class, SendEvent will do nothing. EventSent event will be fired for each event sent with SendEvent holding the MIDI event. The value of DeltaTime property of MIDI events will be ignored, events will be sent to device immediately. To take delta-times into account, use Playback class (read Playback section to learn more).

If you need to interrupt all currently sounding notes, call the TurnAllNotesOff method which will send Note Off events on all channels for all note numbers (kind of "panic button" on MIDI devices).

See API overview section for common members of a MIDI device class that are inherited by OutputDevice from the base class MidiDevice.

Small example that shows sending MIDI data:

C#
using System;
using Melanchall.DryWetMidi.Multimedia;
using Melanchall.DryWetMidi.Core;

// ...

using (var outputDevice = OutputDevice.GetByName("Some MIDI device"))
{
    outputDevice.EventSent += OnEventSent;

    outputDevice.SendEvent(new NoteOnEvent());
    outputDevice.SendEvent(new NoteOffEvent());
}

// ...

private void OnEventSent(object sender, MidiEventSentEventArgs e)
{
    var midiDevice = (MidiDevice)sender;
    Console.WriteLine($"Event sent to '{midiDevice.Name}' at {DateTime.Now}: {e.Event}");
}

Note that you should always take care about disposing an OutputDevice, i.e., use it inside using block or call Dispose manually. Without it, all resources taken by the device will live until GC collects them via finalizer of the OutputDevice. It means that sometimes, you will not be able to use different instances of the same device across multiple applications or different pieces of a program.

First call of SendEvent method can take some time for allocating resources for device, so if you want to eliminate this operation on sending a MIDI event, you can call PrepareForEventsSending method before any MIDI event will be sent.

[^] top

DevicesConnector

To connect one MIDI device to another, there is DevicesConnector class.

Device connector connects an IInputDevice with multiple IOutputDevice objects. To get an instance of DevicesConnector class, you can use either its constructor or Connect extension method on IInputDevice. You must call Connect method of the DevicesConnector to make MIDI data actually go from input device to output devices.

The image below shows how devices will be connected in DryWetMIDI:Image 2

The following small example shows basic usage of DevicesConnector:

C#
using Melanchall.DryWetMidi.Multimedia;

// ...

using (var inputDevice = InputDevice.GetByName("MIDI A"))
using (var outputDevice = OutputDevice.GetByName("MIDI B"))
{
    var devicesConnector = new DevicesConnector(inputDevice, outputDevice);
    devicesConnector.Connect();
}

But to send MIDI data, we need an OutputDevice. So below is a complete example of transferring MIDI events between devices:

C#
using System;
using Melanchall.DryWetMidi.Multimedia;
using Melanchall.DryWetMidi.Core;

// ...

using (var inputB = InputDevice.GetByName("MIDI B"))
{
    inputB.EventReceived += OnEventReceived;
    inputB.StartEventsListening();

    using (var outputA = OutputDevice.GetByName("MIDI A"))
    {
        outputA.EventSent += OnEventSent;

        using (var inputA = InputDevice.GetByName("MIDI A"))
        using (var outputB = OutputDevice.GetByName("MIDI B"))
        {
            var devicesConnector = inputA.Connect(outputB);
            devicesConnector.Connect();

            // These events will be handled by OnEventSent on MIDI A and
            // OnEventReceived on MIDI B
            outputA.SendEvent(new NoteOnEvent());
            outputA.SendEvent(new NoteOffEvent());
        }
    }
}

// ...

private void OnEventReceived(object sender, MidiEventReceivedEventArgs e)
{
    var midiDevice = (MidiDevice)sender;
    Console.WriteLine($"Event received from '{midiDevice.Name}' at {DateTime.Now}: {e.Event}");
}

private void OnEventSent(object sender, MidiEventSentEventArgs e)
{
    var midiDevice = (MidiDevice)sender;
    Console.WriteLine($"Event sent to '{midiDevice.Name}' at {DateTime.Now}: {e.Event}");
}

Don't forget to call StartEventsListening on InputDevice to make sure EventReceived will be fired.

[^] top

Playback

Playback class allows to play MIDI events via an IOutputDevice. In other words, it sends MIDI data to output MIDI device taking events delta-times into account. To get an instance of the Playback, you must use its constructor passing collection of MIDI events, tempo map and output device:

C#
using Melanchall.DryWetMidi.Devices;
using Melanchall.DryWetMidi.Core;
using Melanchall.DryWetMidi.Interaction;

var eventsToPlay = new MidiEvent[]
{
    new NoteOnEvent(),
    new NoteOffEvent
    {
        DeltaTime = 100
    }
};

using (var outputDevice = OutputDevice.GetByName("Output MIDI device"))
using (var playback = new Playback(eventsToPlay, TempoMap.Default, outputDevice))
{
    // ...
}

There are also extension methods GetPlayback for TrackChunk, IEnumerable<TrackChunk>, MidiFile and Pattern classes which simplify obtaining a playback object for MIDI file entities and musical composition created with patterns:

C#
using (var outputDevice = OutputDevice.GetByName("Output MIDI device"))
using (var playback = MidiFile.Read("Some MIDI file.mid").GetPlayback(outputDevice))
{
    // ...
}

GetDuration method returns the total duration of a playback in the specified format.

There are two approaches of playing MIDI data: blocking and non-blocking.

Blocking Playback

If you call Play method of the Playback, the calling thread will be blocked until the entire collection of MIDI events will be sent to MIDI device. Note that execution of this method will be infinite if the Loop property is set to true. See playback properties below to learn more.

There are also extension methods Play for TrackChunk, IEnumerable<TrackChunk>, MidiFile and Pattern classes which simplify playing MIDI file entities and musical composition created with patterns:

C#
using (var outputDevice = OutputDevice.GetByName("Output MIDI device"))
{
    MidiFile.Read("Some MIDI file.mid").Play(outputDevice);

    // ...
}

Non-Blocking Playback

Is you call Start method of the Playback, execution of the calling thread will continue immediately after the method is called. To stop playback, use Stop method. Note that there is no Pause method since it is useless. Stop leaves playback at the point where the method was called. To move to the start of the playback, use MoveToStart method described in the Time management section below.

You should be very careful with this approach and using block. The example below shows the case where part of MIDI data will not be played because playback is disposed before the last MIDI event will be sent to output device:

C#
using (var outputDevice = OutputDevice.GetByName("Output MIDI device"))
using (var playback = MidiFile.Read("Some MIDI file.mid").GetPlayback(outputDevice))
{
    playback.Start();

    // ...
}

With non-blocking approach, it is recommended to call Dispose manually after you've finished work with playback object.

After playback finished, the Finished event will be fired. Started and Stopped events will be fired on Start (Play) and Stop calls respectively.

Playback Properties

Let's see some public properties of the Playback class. Please refer to the Playback documentation to see full API reference.

Loop

Gets or sets a value indicating whether playing should automatically start from the first event after the last one played. If you set it to true and call Play method, calling thread will be blocked forever. The default value is false.

Speed

Gets or sets the speed of events playing. 1.0 means normal speed which is the default. For example, to play MIDI data twice slower, this property should be set to 0.5. Pass 10.0 to play MIDI events ten times faster.

InterruptNotesOnStop

Gets or sets a value indicating whether currently playing notes must be stopped when Stop method called on playback.

TrackNotes

Gets or sets a value indicating whether notes must be tracked or not. If false, notes will be treated as just Note On/Note Off events. If true, notes will be treated as lengthed objects.

If playback stopped in middle of a note, then Note Off event will be sent on stop, and Note On – on playback start. If one of time management methods is called:

  • and old playback's position was on a note, then Note Off will be send for that note;
  • and new playback's position is on a note, then Note On will be send for that note.

IsRunning

Gets a value indicating whether playing is currently running or not.

NoteCallback

Gets or sets a callback used to process notes as they are about to be played.

EventCallback

Gets or sets a callback used to process MIDI events as they are about to be played.

Snapping

Provides a way to manage snap points for playback. See Snapping section below to learn more.

Snapping

Snapping property of the Playback gives an instance of the PlaybackSnapping class which manages snap points for playback. Snap points are markers at speicified times which can be used to move to (see Time management section below).

PlaybackSnapping provides following members:

  • IEnumerable<SnapPoint> SnapPoints

Returns all snap points currently set for playback (including disabled ones).

  • SnapPoint<TData> AddSnapPoint<TData>(ITimeSpan time, TData data)

Add a snap point with the specified data at given time. The data will be available through Data property of the snap point returned by the method.

  • SnapPoint<Guid> AddSnapPoint(ITimeSpan time)

Add a snap point at the specified time without user data. Data will hold unique Guid value.

  • RemoveSnapPoint<TData>(SnapPoint<TData> snapPoint)

Remove a snap point.

  • RemoveSnapPointsByData<TData>(Predicate<TData> predicate)

Remove all snap points that match the conditions defined by the specified predicate.

  • SnapPointsGroup SnapToGrid(IGrid grid)

Add snap points at times defined by the specified grid. Implementations of IGrid are SteppedGrid (grid where times are defined by collection of repeating steps) and ArbitraryGrid (grid where times are specified explicitly). An instance of SnapPointsGroup will be returned which can be used to manage all the snap points added by the method.

  • SnapPointsGroup SnapToNotesStarts()

Adds snap points at start times of notes. Returned group can be used to manage added snap points.

  • SnapPointsGroup SnapToNotesEnds()

Adds snap points at end times of notes. Returned group can be used to manage added snap points.

SnapPoint

SnapPoint<> returned by the methods above is inherited from SnapPoint and just adds Data property that holds user data attached to a snap point. SnapPoint has following members:

IsEnabled

Gets or sets a value indicating whether a snap point is enabled or not. Can be used to turn a snap point on or off. If a snap point is disabled, it won't participate in time management operations.

Time

Gets the time of a snap point as an instance of TimeSpan.

SnapPointsGroup

Gets the group a snap point belongs to as an instance of SnapPointsGroup. It will be null for snap points added by AddSnapPoint method.

SnapPointsGroup

SnapPointsGroup has IsEnabled property that is similar to that the SnapPoint has. If a snap points group is disabled, its snap points won't participate in time management operations.

Time Management

You have several options to manipulate by the current time of playback:

  • GetCurrentTime

    Returns the current time of a playback in the specified format.

  • MoveToStart

    Sets playback position to the beginning of the MIDI data.

  • MoveToTime

    Sets playback position to the specified time from the beginning of the MIDI data. If new position is greater than playback duration, position will be set to the end of the playback.

  • MoveForward

    Shifts playback position forward by the specified step. If new position is greater than playback duration, position will be set to the end of the playback.

  • MoveBack

    Shifts playback position back by the specified step. If step is greater than the elapsed time of playback, position will be set to the start of the playback.

  • MoveToSnapPoint(SnapPoint snapPoint)

Sets playback position to the time of the specified snap point. If snapPoint is disabled, nothing will happen.

  • MoveToPreviousSnapPoint(SnapPointsGroup snapPointsGroup)

Sets playback position to the time of the previous snap point (relative to the current time of playback) that belongs to snapPointsGroup.

  • MoveToPreviousSnapPoint

Sets playback position to the time of the previous snap point (relative to the current time of playback).

  • MoveToNextSnapPoint(SnapPointsGroup snapPointsGroup)

Sets playback position to the time of the next snap point (relative to the current time of playback) that belongs to the specified SnapPointsGroup.

  • MoveToNextSnapPoint

Sets playback position to the time of the next snap point (relative to the current time of playback).

You don't need to call Stop method if you want to call any method that changes the current playback position.

[^] top

Recording

To capture MIDI data from an input MIDI device, you can use Recording class which will collect incoming MIDI events. To start recording, you need to create an instance of the Recording passing tempo map and input device to its constructor:

C#
using Melanchall.DryWetMidi.Multimedia;
using Melanchall.DryWetMidi.Interaction;

// ...

using (var inputDevice = InputDevice.GetByName("Input MIDI device"))
{
    var recording = new Recording(TempoMap.Default, inputDevice);

    // ...
}

Don't forget to call StartEventsListening on InputDevice before you start recording since Recording does nothing with the device you've specified.

To start recording, call Start method. To stop it, call Stop method. You can resume recording after it has been stopped by calling Start again. To check whether recording is currently running, get a value of the IsRunning property. Start and Stop methods fire Started and Stopped events respectively.

You can get recorded events as IEnumerable<TimedEvent> with the GetEvents method.

GetDuration method returns the total duration of a recording in the specified format.

Take a look at a small example of MIDI data recording:

C#
using (var inputDevice = InputDevice.GetByName("Input MIDI device"))
{
    var recording = new Recording(TempoMap.Default, inputDevice);

    inputDevice.StartEventsListening();
    recording.Start();

    // ...

    recording.Stop();

    var recordedFile = recording.ToFile();
    recording.Dispose();
    recordedFile.Write("Recorded data.mid");
}

[^] top

VirtualDevice

VirtualDevice available for macOS only at now and provides a way t odynamically create virtual MIDI devices that are loopback devices (virtual cables). In other words virtual device is paired input and output device. Each MIDI event sent to the output subdevice of a virtual one will be redirected to the input subdevice, so MIDI data will be sent back without any transformation.

Please see Virtual device article of the library documentation to learn more.

[^] top

DevicesWatcher

DevicesWatcher also available for macOS only. It allows to monitor whether a MIDI device is added to (plugged) or removed from (unplugged) the system.

Please see Devices watcher article of the library documentation to learn more.

[^] top

Links

[^] top

History

  • 22d October, 2021
    • Article updated to reflect changes introduced in version 6.0.0 of the library
  • 27th June, 2021
    • Added some notes about the article is not API reference
  • 23rd November, 2019
    • Article updated to reflect changes introduced in version 5.0.0 of the library
  • 10th May, 2019
    • Article updated to reflect changes introduced in version 4.1.0 of the library
  • 31st January, 2019
    • Article submitted

[^] top

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)